Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)

Bug: 166295507
Merged-In: I88f54d5cf388aeacbf1da48e3e5f1e89f37ba836
Change-Id: I2e4012ae6bf1cf4e5e5cb76790cd2451e313febc
diff --git a/Android.bp b/Android.bp
index 4c38205..e132854 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,3 +30,31 @@
     manifest: "tests/tapl/AndroidManifest.xml",
     platform_apis: true,
 }
+
+java_library_static {
+    name: "launcher_log_protos_lite",
+    srcs: [
+        "protos/*.proto",
+        "proto_overrides/*.proto",
+    ],
+    sdk_version: "current",
+    proto: {
+        type: "lite",
+        local_include_dirs:[
+            "protos",
+            "proto_overrides",
+        ],
+    },
+    static_libs: ["libprotobuf-java-lite"],
+}
+
+java_library {
+    name: "LauncherPluginLib",
+
+    static_libs: ["PluginCoreLib"],
+
+    srcs: ["src_plugins/**/*.java"],
+
+    sdk_version: "current",
+    min_sdk_version: "28",
+}
diff --git a/Android.mk b/Android.mk
index 6b6e4fe..7805b32 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,24 +17,6 @@
 LOCAL_PATH := $(call my-dir)
 
 #
-# Build rule for plugin lib (needed to write a plugin).
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT2_ONLY := true
-LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES:= PluginCoreLib
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src_plugins)
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 28
-LOCAL_MODULE := LauncherPluginLib
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#
 # Build rule for Launcher3 dependencies lib.
 #
 include $(CLEAR_VARS)
@@ -48,7 +30,9 @@
     androidx.preference_preference \
     iconloader_base
 
-LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    LauncherPluginLib \
+    launcher_log_protos_lite
 
 LOCAL_SRC_FILES := \
     $(call all-proto-files-under, protos) \
@@ -78,14 +62,15 @@
 LOCAL_USE_AAPT2 := true
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    Launcher3CommonDepsLib \
-    SecondaryDisplayLauncherLib
+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, src_flags)
+    $(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
@@ -146,7 +131,11 @@
 LOCAL_AAPT2_ONLY := true
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    SystemUI-statsd \
+    SystemUISharedLib \
+    launcherprotosnano \
+    launcher_log_protos_lite
 ifneq (,$(wildcard frameworks/base))
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
@@ -155,15 +144,12 @@
 endif
 LOCAL_MODULE := Launcher3QuickStepLib
 LOCAL_PRIVILEGED_MODULE := true
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    Launcher3CommonDepsLib \
-    SecondaryDisplayLauncherLib
+LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
 
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
     $(call all-java-files-under, quickstep/src) \
     $(call all-java-files-under, quickstep/recents_ui_overrides/src) \
-    $(call all-java-files-under, src_flags) \
     $(call all-java-files-under, src_shortcuts_overrides)
 
 LOCAL_RESOURCE_DIR := \
@@ -202,7 +188,7 @@
     $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
-    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
@@ -218,7 +204,11 @@
 LOCAL_USE_AAPT2 := true
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    SystemUI-statsd \
+    SystemUISharedLib \
+    launcherprotosnano \
+    launcher_log_protos_lite
 ifneq (,$(wildcard frameworks/base))
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
@@ -244,58 +234,12 @@
 LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3GoIconRecents
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
     $(LOCAL_PATH)/go/AndroidManifest.xml \
-    $(LOCAL_PATH)/AndroidManifest.xml \
-    $(LOCAL_PATH)/AndroidManifest-common.xml
-
-LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
-LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
-include $(BUILD_PACKAGE)
-
-#
-# Build rule for Launcher3 Go app with quickstep and Go-specific
-# version of recents for Android Go devices.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
-ifneq (,$(wildcard frameworks/base))
-  LOCAL_PRIVATE_PLATFORM_APIS := true
-else
-  LOCAL_SDK_VERSION := system_current
-  LOCAL_MIN_SDK_VERSION := 26
-endif
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-java-files-under, quickstep/src) \
-    $(call all-java-files-under, go/src) \
-    $(call all-java-files-under, go/quickstep/src)
-
-LOCAL_RESOURCE_DIR := \
-    $(LOCAL_PATH)/quickstep/res \
-    $(LOCAL_PATH)/go/res \
-    $(LOCAL_PATH)/go/quickstep/res
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-LOCAL_PROGUARD_ENABLED := full
-
-LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3Go Launcher3QuickStep
-LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
-
-LOCAL_FULL_LIBS_MANIFEST_FILES := \
-    $(LOCAL_PATH)/go/AndroidManifest.xml \
-    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 555cc73..dd0fc21 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -44,8 +44,11 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
-
+    <!-- TODO(b/150802536): Enabled only for ENABLE_FIXED_ROTATION_TRANSFORM feature flag -->
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    
     <!--
     Permissions required for read/write access to the workspace data. These permission name
     should not conflict with that defined in other apps, as such an app should embed its package
@@ -70,6 +73,7 @@
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
         android:fullBackupOnly="true"
+        android:backupInForeground="true"
         android:fullBackupContent="@xml/backupscheme"
         android:hardwareAccelerated="true"
         android:icon="@drawable/ic_launcher_home"
@@ -134,14 +138,6 @@
         </activity>
 
         <!--
-        Should point to the content provider which can be used to dump Launcher3 compatible
-        worspace configuration to the dump's file descriptor by using launcher_dump.proto
-        -->
-        <meta-data
-            android:name="com.android.launcher3.launcher_dump_provider"
-            android:value="com.android.launcher3.LauncherProvider" />
-
-        <!--
         The settings provider contains Home's data, like the workspace favorites. The permissions
         should be changed to what is defined above. The authorities should also be changed to
         represent the package name.
@@ -160,8 +156,8 @@
         <provider
             android:name="com.android.launcher3.graphics.GridOptionsProvider"
             android:authorities="${packageName}.grid_control"
-            android:enabled="false"
-            android:exported="true" />
+            android:exported="true"
+            android:enabled="false" />
 
         <!--
         The settings activity. To extend point settings_fragment_name to appropriate fragment class
@@ -184,5 +180,20 @@
             android:writePermission="android.permission.WRITE_SECURE_SETTINGS"
             android:exported="true"
             android:enabled="false" />
+
+        <!--
+        Launcher activity for secondary display
+        -->
+        <activity
+            android:name="com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher"
+            android:theme="@style/AppTheme"
+            android:launchMode="singleTop"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.SECONDARY_HOME" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/OWNERS b/OWNERS
index 538ca33..3069afa 100644
--- a/OWNERS
+++ b/OWNERS
@@ -8,8 +8,26 @@
 hyunyoungs@google.com
 mrcasey@google.com
 sunnygoyal@google.com
+awickham@google.com
 twickham@google.com
 winsonc@google.com
+zakcohen@google.com
+santie@google.com
+vadimt@google.com
+mett@google.com
+jonmiranda@google.com
+pinyaoting@google.com
+sfufa@google.com
+gwasserman@google.com
+jamesoleary@google.com
+joshtrask@google.com
+mrenouf@google.com
+mkephart@google.com
+hwwang@google.com
+tracyzhou@google.com
+peanutbutter@google.com
+xuqiu@google.com
+sreyasr@google.com
 
-per-file FeatureFlags.java = sunnygoyal@google.com, adamcohen@google.com
-per-file BaseFlags.java = sunnygoyal@google.com, adamcohen@google.com
+per-file FeatureFlags.java, globs = set noparent
+per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, zakcohen@google.com, mrcasey@google.com, adamcohen@google.com, hyunyoungs@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f3db20e..9123959 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,2 @@
 [Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/SecondaryDisplayLauncher/Android.mk b/SecondaryDisplayLauncher/Android.mk
deleted file mode 100644
index 7f305bb..0000000
--- a/SecondaryDisplayLauncher/Android.mk
+++ /dev/null
@@ -1,39 +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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT2_ONLY := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := com.google.android.material_material
-
-LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_MODULE := SecondaryDisplayLauncherLib
-LOCAL_PRIVILEGED_MODULE := true
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/SecondaryDisplayLauncher/AndroidManifest.xml b/SecondaryDisplayLauncher/AndroidManifest.xml
deleted file mode 100644
index ebf6b02..0000000
--- a/SecondaryDisplayLauncher/AndroidManifest.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 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.
-*/
--->
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.launcher3">
-
-    <application>
-
-        <activity
-            android:name="com.android.launcher3.SecondaryDisplayLauncher"
-            android:theme="@style/SecondaryLauncherTheme"
-            android:launchMode="singleTop"
-            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"
-            android:enabled="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.SECONDARY_HOME" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-
-    </application>
-</manifest>
diff --git a/SecondaryDisplayLauncher/res/drawable/ic_settings.xml b/SecondaryDisplayLauncher/res/drawable/ic_settings.xml
deleted file mode 100644
index c269c3b..0000000
--- a/SecondaryDisplayLauncher/res/drawable/ic_settings.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<vector android:height="24dp" android:tint="#FFFFFF"
-        android:viewportHeight="24.0" android:viewportWidth="24.0"
-        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="#FF000000" android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
-</vector>
diff --git a/SecondaryDisplayLauncher/res/layout-sw600dp/secondary_display_launcher.xml b/SecondaryDisplayLauncher/res/layout-sw600dp/secondary_display_launcher.xml
deleted file mode 100644
index 46f1674..0000000
--- a/SecondaryDisplayLauncher/res/layout-sw600dp/secondary_display_launcher.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/RootView"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/launcher_bg_color"
-    android:fitsSystemWindows="true" >
-
-    <GridView
-        android:id="@+id/pinned_app_grid"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/app_grid_margin_top"
-        android:layout_marginStart="@dimen/app_grid_margin_left"
-        android:layout_marginEnd="@dimen/app_grid_margin_right"
-        android:columnWidth="@dimen/app_list_col_width"
-        android:verticalSpacing="@dimen/app_list_horizontal_spacing"
-        android:horizontalSpacing="@dimen/app_list_vertical_spacing"
-        android:numColumns="auto_fit" />
-
-    <ImageButton
-        android:id="@+id/OptionsButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|start"
-        android:layout_marginStart="@dimen/options_button_margin"
-        android:layout_marginBottom="@dimen/options_button_margin"
-        android:src="@drawable/ic_settings"
-        android:background="@null"/>
-
-    <FrameLayout
-        android:layout_width="@dimen/app_picker_width"
-        android:layout_height="@dimen/app_picker_height"
-        android:layout_gravity="bottom|end"
-        android:layout_margin="@dimen/app_picker_fab_margin">
-
-        <include layout="@layout/app_picker_layout"/>
-    </FrameLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/SecondaryDisplayLauncher/res/layout-sw720dp/secondary_display_launcher.xml b/SecondaryDisplayLauncher/res/layout-sw720dp/secondary_display_launcher.xml
deleted file mode 100644
index 6653a77..0000000
--- a/SecondaryDisplayLauncher/res/layout-sw720dp/secondary_display_launcher.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/RootView"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/launcher_bg_color"
-    android:fitsSystemWindows="true" >
-
-    <GridView
-        android:id="@+id/pinned_app_grid"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/app_grid_margin_top"
-        android:layout_marginStart="@dimen/app_grid_margin_left"
-        android:layout_marginEnd="@dimen/app_grid_margin_right"
-        android:columnWidth="@dimen/app_list_col_width"
-        android:verticalSpacing="@dimen/app_list_horizontal_spacing"
-        android:horizontalSpacing="@dimen/app_list_vertical_spacing"
-        android:numColumns="auto_fit" />
-
-    <ImageButton
-        android:id="@+id/OptionsButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|start"
-        android:layout_marginStart="@dimen/options_button_margin"
-        android:layout_marginBottom="@dimen/options_button_margin"
-        android:src="@drawable/ic_settings"
-        android:background="@null"/>
-
-    <FrameLayout
-        android:layout_width="@dimen/app_picker_width"
-        android:layout_height="@dimen/app_picker_height"
-        android:layout_gravity="bottom|end"
-        android:layout_marginEnd="@dimen/app_picker_fab_margin"
-        android:layout_marginBottom="@dimen/app_picker_fab_margin">
-
-        <include layout="@layout/app_picker_layout"/>
-    </FrameLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/SecondaryDisplayLauncher/res/layout/app_picker_dialog.xml b/SecondaryDisplayLauncher/res/layout/app_picker_dialog.xml
deleted file mode 100644
index 563a1af..0000000
--- a/SecondaryDisplayLauncher/res/layout/app_picker_dialog.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <GridView
-        android:id="@+id/picker_app_grid"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/app_grid_margin_top"
-        android:layout_marginStart="@dimen/app_grid_margin_left"
-        android:layout_marginEnd="@dimen/app_grid_margin_right"
-        android:columnWidth="@dimen/app_list_col_width"
-        android:verticalSpacing="@dimen/app_list_horizontal_spacing"
-        android:horizontalSpacing="@dimen/app_list_vertical_spacing"
-        android:numColumns="auto_fit" />
-</FrameLayout>
diff --git a/SecondaryDisplayLauncher/res/layout/app_picker_layout.xml b/SecondaryDisplayLauncher/res/layout/app_picker_layout.xml
deleted file mode 100644
index 20f85b1..0000000
--- a/SecondaryDisplayLauncher/res/layout/app_picker_layout.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <com.google.android.material.circularreveal.cardview.CircularRevealCardView
-        android:id="@+id/FloatingSheet"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="invisible">
-
-        <GridView
-            android:id="@+id/app_grid"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_marginTop="@dimen/app_grid_margin_top"
-            android:columnWidth="@dimen/app_list_col_width"
-            android:verticalSpacing="@dimen/app_list_horizontal_spacing"
-            android:horizontalSpacing="@dimen/app_list_vertical_spacing"
-            android:numColumns="auto_fit" />
-    </com.google.android.material.circularreveal.cardview.CircularRevealCardView>
-
-    <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/FloatingActionButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|end"
-        android:src="@drawable/ic_apps"/>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/SecondaryDisplayLauncher/res/layout/secondary_display_launcher.xml b/SecondaryDisplayLauncher/res/layout/secondary_display_launcher.xml
deleted file mode 100644
index 49cd499..0000000
--- a/SecondaryDisplayLauncher/res/layout/secondary_display_launcher.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/RootView"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/launcher_bg_color"
-    android:fitsSystemWindows="true" >
-
-    <GridView
-        android:id="@+id/pinned_app_grid"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/app_grid_margin_top"
-        android:layout_marginStart="@dimen/app_grid_margin_left"
-        android:layout_marginEnd="@dimen/app_grid_margin_right"
-        android:columnWidth="@dimen/app_list_col_width"
-        android:verticalSpacing="@dimen/app_list_horizontal_spacing"
-        android:horizontalSpacing="@dimen/app_list_vertical_spacing"
-        android:numColumns="auto_fit" />
-
-    <ImageButton
-        android:id="@+id/OptionsButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|start"
-        android:layout_marginStart="@dimen/options_button_margin"
-        android:layout_marginBottom="@dimen/options_button_margin"
-        android:src="@drawable/ic_settings"
-        android:background="@null"/>
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_gravity="bottom|end"
-        android:layout_margin="@dimen/app_picker_fab_margin">
-
-        <include layout="@layout/app_picker_layout"/>
-    </FrameLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/SecondaryDisplayLauncher/res/menu/context_menu.xml b/SecondaryDisplayLauncher/res/menu/context_menu.xml
deleted file mode 100644
index 6263842..0000000
--- a/SecondaryDisplayLauncher/res/menu/context_menu.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/add_app_shortcut"
-          android:title="@string/add_app_shortcut" />
-    <item android:id="@+id/set_wallpaper"
-          android:title="@string/set_wallpaper" />
-</menu>
diff --git a/SecondaryDisplayLauncher/res/values-af/strings.xml b/SecondaryDisplayLauncher/res/values-af/strings.xml
deleted file mode 100644
index b544be7a..0000000
--- a/SecondaryDisplayLauncher/res/values-af/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Kon nie die aktiwiteit begin nie"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Voeg programkortpad by"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Stel muurpapier"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-am/strings.xml b/SecondaryDisplayLauncher/res/values-am/strings.xml
deleted file mode 100644
index 71854ad..0000000
--- a/SecondaryDisplayLauncher/res/values-am/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"እንቅስቃሴውን ማስጀመር አልተቻለም"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"የመተግበሪያ አቋራጭ ያክሉ"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ልጣፍ አዘጋጅ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ar/strings.xml b/SecondaryDisplayLauncher/res/values-ar/strings.xml
deleted file mode 100644
index aa34c7d..0000000
--- a/SecondaryDisplayLauncher/res/values-ar/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"تعذَّر تشغيل النشاط."</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"إضافة اختصار التطبيق"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ضبط الخلفية"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-as/strings.xml b/SecondaryDisplayLauncher/res/values-as/strings.xml
deleted file mode 100644
index d199a26..0000000
--- a/SecondaryDisplayLauncher/res/values-as/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"কাৰ্যকলাপটো লঞ্চ কৰিব পৰা নগ’ল"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"এপৰ শ্বর্টকাট যোগ কৰক"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ৱালপেপাৰ ছেট কৰক"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-az/strings.xml b/SecondaryDisplayLauncher/res/values-az/strings.xml
deleted file mode 100644
index cee70a0..0000000
--- a/SecondaryDisplayLauncher/res/values-az/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Fəaliyyəti başlatmaq mümkün olmadı"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Tətbiq qısayolu əlavə edin"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Divar kağızı ayarlayın"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-b+sr+Latn/strings.xml b/SecondaryDisplayLauncher/res/values-b+sr+Latn/strings.xml
deleted file mode 100644
index a8859d9..0000000
--- a/SecondaryDisplayLauncher/res/values-b+sr+Latn/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Pokretanje aktivnosti nije uspelo"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Dodaj prečicu za aplikaciju"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Podesite pozadinu"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-be/strings.xml b/SecondaryDisplayLauncher/res/values-be/strings.xml
deleted file mode 100644
index 3df3760..0000000
--- a/SecondaryDisplayLauncher/res/values-be/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Не ўдалося запусціць дзеянне"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Дадаць ярлык праграмы"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Устанавіць шпалеры"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-bg/strings.xml b/SecondaryDisplayLauncher/res/values-bg/strings.xml
deleted file mode 100644
index 4474815..0000000
--- a/SecondaryDisplayLauncher/res/values-bg/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Активността не можа да бъде стартирана"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Добавяне на пряк път към приложението"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Задаване на тапет"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-bn/strings.xml b/SecondaryDisplayLauncher/res/values-bn/strings.xml
deleted file mode 100644
index 7322691..0000000
--- a/SecondaryDisplayLauncher/res/values-bn/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"অ্যাক্টিভিটি চালু করা যায়নি"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"অ্যাপ শর্টকাট যোগ করুন"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ওয়ালপেপার সেট করুন"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-bs/strings.xml b/SecondaryDisplayLauncher/res/values-bs/strings.xml
deleted file mode 100644
index 1e59d33..0000000
--- a/SecondaryDisplayLauncher/res/values-bs/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Pokretanje aktivnosti nije uspjelo"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Dodaj prečicu aplikacije"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Postavi pozadinsku sliku"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ca/strings.xml b/SecondaryDisplayLauncher/res/values-ca/strings.xml
deleted file mode 100644
index c0274d1..0000000
--- a/SecondaryDisplayLauncher/res/values-ca/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"No s\'ha pogut iniciar l\'activitat"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Afegeix una drecera d\'aplicació"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Estableix el fons de pantalla"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-cs/strings.xml b/SecondaryDisplayLauncher/res/values-cs/strings.xml
deleted file mode 100644
index 92ed5fa..0000000
--- a/SecondaryDisplayLauncher/res/values-cs/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Aktivitu nelze zahájit"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Přidat zkratku aplikace"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Nastavení tapety"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-da/strings.xml b/SecondaryDisplayLauncher/res/values-da/strings.xml
deleted file mode 100644
index 16bdb33..0000000
--- a/SecondaryDisplayLauncher/res/values-da/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Aktiviteten kunne ikke startes"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Tilføj appgenvej"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Angiv baggrund"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-de/strings.xml b/SecondaryDisplayLauncher/res/values-de/strings.xml
deleted file mode 100644
index 3617a5b..0000000
--- a/SecondaryDisplayLauncher/res/values-de/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Aktivität konnte nicht gestartet werden"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"App-Verknüpfung hinzufügen"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Hintergrund festlegen"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-el/strings.xml b/SecondaryDisplayLauncher/res/values-el/strings.xml
deleted file mode 100644
index 8d19d09..0000000
--- a/SecondaryDisplayLauncher/res/values-el/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Δεν ήταν δυνατή η εκκίνηση της δραστηριότητας"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Προσθήκη συντόμευσης εφαρμογής"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Ορισμός ταπετσαρίας"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-en-rAU/strings.xml b/SecondaryDisplayLauncher/res/values-en-rAU/strings.xml
deleted file mode 100644
index 8d8c419..0000000
--- a/SecondaryDisplayLauncher/res/values-en-rAU/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Couldn\'t launch the activity"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Add app shortcut"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Set wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-en-rCA/strings.xml b/SecondaryDisplayLauncher/res/values-en-rCA/strings.xml
deleted file mode 100644
index 8d8c419..0000000
--- a/SecondaryDisplayLauncher/res/values-en-rCA/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Couldn\'t launch the activity"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Add app shortcut"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Set wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-en-rGB/strings.xml b/SecondaryDisplayLauncher/res/values-en-rGB/strings.xml
deleted file mode 100644
index 8d8c419..0000000
--- a/SecondaryDisplayLauncher/res/values-en-rGB/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Couldn\'t launch the activity"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Add app shortcut"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Set wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-en-rIN/strings.xml b/SecondaryDisplayLauncher/res/values-en-rIN/strings.xml
deleted file mode 100644
index 8d8c419..0000000
--- a/SecondaryDisplayLauncher/res/values-en-rIN/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Couldn\'t launch the activity"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Add app shortcut"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Set wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-en-rXC/strings.xml b/SecondaryDisplayLauncher/res/values-en-rXC/strings.xml
deleted file mode 100644
index da69193..0000000
--- a/SecondaryDisplayLauncher/res/values-en-rXC/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‏‎‎‎‏‎‎‏‎‎‏‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‏‎‎‎‎‏‎‏‏‏‏‎‎‎‏‏‎‏‏‎‎‎‏‎‏‏‎‏‎‎Couldn\'t launch the activity‎‏‎‎‏‎"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‎‎‏‎‏‏‏‎‏‏‎Add app shortcut‎‏‎‎‏‎"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‎‏‏‎‏‎‎‏‏‎‏‎‎‎‎‏‏‎‏‏‎‎‎‎‎‎‎‎‎Set wallpaper‎‏‎‎‏‎"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-es-rUS/strings.xml b/SecondaryDisplayLauncher/res/values-es-rUS/strings.xml
deleted file mode 100644
index ff6772b..0000000
--- a/SecondaryDisplayLauncher/res/values-es-rUS/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"No se pudo iniciar la actividad"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Agregar acceso directo a app"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Establecer fondo de pantalla"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-es/strings.xml b/SecondaryDisplayLauncher/res/values-es/strings.xml
deleted file mode 100644
index 0654dcb..0000000
--- a/SecondaryDisplayLauncher/res/values-es/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"No se ha podido iniciar la acción"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Añadir acceso directo a la aplicación"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Establecer fondo de pantalla"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-et/strings.xml b/SecondaryDisplayLauncher/res/values-et/strings.xml
deleted file mode 100644
index 3410fd4..0000000
--- a/SecondaryDisplayLauncher/res/values-et/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Tegevust ei saanud käivitada"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Lisa rakenduse otsetee"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Määra taustapilt"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-eu/strings.xml b/SecondaryDisplayLauncher/res/values-eu/strings.xml
deleted file mode 100644
index d7abe33..0000000
--- a/SecondaryDisplayLauncher/res/values-eu/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Ezin izan da abiarazi jarduera"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Gehitu aplikaziorako lasterbidea"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Ezarri horma-papera"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-fa/strings.xml b/SecondaryDisplayLauncher/res/values-fa/strings.xml
deleted file mode 100644
index 4d3ec4d..0000000
--- a/SecondaryDisplayLauncher/res/values-fa/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"فعالیت راه‌اندازی نشد"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"افزودن میان‌بر برنامه"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"تنظیم کاغذدیواری"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-fi/strings.xml b/SecondaryDisplayLauncher/res/values-fi/strings.xml
deleted file mode 100644
index e56f67a..0000000
--- a/SecondaryDisplayLauncher/res/values-fi/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Käynnistäminen epäonnistui"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Lisää sovelluksen pikakuvake"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Aseta taustakuva"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-fr-rCA/strings.xml b/SecondaryDisplayLauncher/res/values-fr-rCA/strings.xml
deleted file mode 100644
index f5c9ba5..0000000
--- a/SecondaryDisplayLauncher/res/values-fr-rCA/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Impossible de lancer l\'activité"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Ajouter un raccourci vers l\'application"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Définir le fond d\'écran"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-fr/strings.xml b/SecondaryDisplayLauncher/res/values-fr/strings.xml
deleted file mode 100644
index daa186b..0000000
--- a/SecondaryDisplayLauncher/res/values-fr/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Impossible de lancer l\'activité"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Ajouter un raccourci vers l\'application"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Configurer le fond d\'écran"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-gl/strings.xml b/SecondaryDisplayLauncher/res/values-gl/strings.xml
deleted file mode 100644
index 0bcf969..0000000
--- a/SecondaryDisplayLauncher/res/values-gl/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Non se puido iniciar a actividade"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Engadir atallo da aplicación"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Definir fondo de pantalla"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-gu/strings.xml b/SecondaryDisplayLauncher/res/values-gu/strings.xml
deleted file mode 100644
index 82b4444..0000000
--- a/SecondaryDisplayLauncher/res/values-gu/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"પ્રવૃત્તિ લૉન્ચ કરી શકાઈ નથી"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ઍપ શૉર્ટકટ ઉમેરો"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"વૉલપેપર સેટ કરો"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-hi/strings.xml b/SecondaryDisplayLauncher/res/values-hi/strings.xml
deleted file mode 100644
index 8adb519..0000000
--- a/SecondaryDisplayLauncher/res/values-hi/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"गतिविधि लॉन्च नहीं हो सकी"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ऐप्लिकेशन शॉर्टकट जोड़ें"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"वॉलपेपर सेट करें"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-hr/strings.xml b/SecondaryDisplayLauncher/res/values-hr/strings.xml
deleted file mode 100644
index 87ac874..0000000
--- a/SecondaryDisplayLauncher/res/values-hr/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Pokretanje aktivnosti nije uspjelo"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Dodajte aplikacijski prečac"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Postavljanje pozadine"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-hu/strings.xml b/SecondaryDisplayLauncher/res/values-hu/strings.xml
deleted file mode 100644
index a8870fc..0000000
--- a/SecondaryDisplayLauncher/res/values-hu/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Nem sikerült elindítani a tevékenységet"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Alkalmazás parancsikonjának hozzáadása"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Háttérkép beállítása"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-hy/strings.xml b/SecondaryDisplayLauncher/res/values-hy/strings.xml
deleted file mode 100644
index a64233f..0000000
--- a/SecondaryDisplayLauncher/res/values-hy/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Չհաջողվեց գործարկել գործողությունը"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Ավելացնել հավելվածի դյուրանցումը"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Դարձնել պաստառ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-in/strings.xml b/SecondaryDisplayLauncher/res/values-in/strings.xml
deleted file mode 100644
index f51d238..0000000
--- a/SecondaryDisplayLauncher/res/values-in/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Tidak dapat meluncurkan aktivitas"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Tambahkan pintasan app"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Setel wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-is/strings.xml b/SecondaryDisplayLauncher/res/values-is/strings.xml
deleted file mode 100644
index e8b3e97..0000000
--- a/SecondaryDisplayLauncher/res/values-is/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Ekki tókst að ræsa aðgerðina"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Bæta við flýtileið forrita"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Velja veggfóður"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-it/strings.xml b/SecondaryDisplayLauncher/res/values-it/strings.xml
deleted file mode 100644
index 4941515..0000000
--- a/SecondaryDisplayLauncher/res/values-it/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Impossibile avviare l\'attività"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Aggiungi scorciatoia app"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Imposta sfondo"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-iw/strings.xml b/SecondaryDisplayLauncher/res/values-iw/strings.xml
deleted file mode 100644
index 06b0c42..0000000
--- a/SecondaryDisplayLauncher/res/values-iw/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"לא ניתן היה להפעיל את הפעילות"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"הוספת קיצור דרך של אפליקציה"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"הגדרת טפט"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ja/strings.xml b/SecondaryDisplayLauncher/res/values-ja/strings.xml
deleted file mode 100644
index 3ed7b2b..0000000
--- a/SecondaryDisplayLauncher/res/values-ja/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"アクティビティを開始できませんでした"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"アプリのショートカットを追加"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"壁紙を設定"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ka/strings.xml b/SecondaryDisplayLauncher/res/values-ka/strings.xml
deleted file mode 100644
index ac85f70..0000000
--- a/SecondaryDisplayLauncher/res/values-ka/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"აქტივობის გაშვება ვერ მოხერხდა"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"აპის მალსახმობის დამატება"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ფონის დაყენება"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-kk/strings.xml b/SecondaryDisplayLauncher/res/values-kk/strings.xml
deleted file mode 100644
index f9ac455..0000000
--- a/SecondaryDisplayLauncher/res/values-kk/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Әрекет іске қосылмады"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Қолданба таңбашасын енгізу"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Тұсқағаз орнату"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-km/strings.xml b/SecondaryDisplayLauncher/res/values-km/strings.xml
deleted file mode 100644
index afc050f..0000000
--- a/SecondaryDisplayLauncher/res/values-km/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"មិនអាចចាប់ផ្តើមសកម្មភាពទេ"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"បញ្ចូល​ផ្លូវកាត់​កម្មវិធី"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"កំណត់​ផ្ទាំង​រូបភាព"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-kn/strings.xml b/SecondaryDisplayLauncher/res/values-kn/strings.xml
deleted file mode 100644
index 09c327f..0000000
--- a/SecondaryDisplayLauncher/res/values-kn/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ಚಟುವಟಿಕೆಯನ್ನು ಲಾಂಚ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ಆ್ಯಪ್‌ ಶಾರ್ಟ್‌ಕಟ್ ಸೇರಿಸಿ"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ವಾಲ್‌ಪೇಪರ್ ಹೊಂದಿಸಿ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ko/strings.xml b/SecondaryDisplayLauncher/res/values-ko/strings.xml
deleted file mode 100644
index 6a02ac0..0000000
--- a/SecondaryDisplayLauncher/res/values-ko/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"활동을 실행할 수 없음"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"앱 바로가기 추가"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"배경화면 설정"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ky/strings.xml b/SecondaryDisplayLauncher/res/values-ky/strings.xml
deleted file mode 100644
index 56185fa..0000000
--- a/SecondaryDisplayLauncher/res/values-ky/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Аракет аткарылган жок"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Колдонмого кыска жол кошуу"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Тушкагаз орнотуу"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-lo/strings.xml b/SecondaryDisplayLauncher/res/values-lo/strings.xml
deleted file mode 100644
index 36a6275..0000000
--- a/SecondaryDisplayLauncher/res/values-lo/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ບໍ່ສາມາດເປີດໃຊ້ການເຄື່ອນໄຫວໄດ້"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ເພີ່ມທາງລັດແອັບ"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ຕັ້ງເປັນຮູບພື້ນຫຼັງ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-lt/strings.xml b/SecondaryDisplayLauncher/res/values-lt/strings.xml
deleted file mode 100644
index 8113eb6..0000000
--- a/SecondaryDisplayLauncher/res/values-lt/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Nepavyko paleisti veiklos"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Pridėti programos šaukinį"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Nustatyti ekrano foną"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-lv/strings.xml b/SecondaryDisplayLauncher/res/values-lv/strings.xml
deleted file mode 100644
index e267933..0000000
--- a/SecondaryDisplayLauncher/res/values-lv/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Nevarēja palaist darbību"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Pievienot lietotnes saīsni"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Iestatīt fona tapeti"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-mk/strings.xml b/SecondaryDisplayLauncher/res/values-mk/strings.xml
deleted file mode 100644
index e2cca03..0000000
--- a/SecondaryDisplayLauncher/res/values-mk/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Не можеше да се стартува активноста"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Додајте кратенка за апликација"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Поставете го тапетот"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ml/strings.xml b/SecondaryDisplayLauncher/res/values-ml/strings.xml
deleted file mode 100644
index 864245b..0000000
--- a/SecondaryDisplayLauncher/res/values-ml/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ആക്‌റ്റിവിറ്റി പ്രകാശിപ്പിക്കാനായില്ല"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ആപ്പ് കുറുക്കുവഴികൾ ചേർക്കുക"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"വാൾപേപ്പർ സജ്ജീകരിക്കുക"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-mn/strings.xml b/SecondaryDisplayLauncher/res/values-mn/strings.xml
deleted file mode 100644
index 85fb020..0000000
--- a/SecondaryDisplayLauncher/res/values-mn/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Үйл ажиллагааг эхлүүж чадсангүй"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Аппын товчлол нэмэх"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Ханын зураг тохируулах"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-mr/strings.xml b/SecondaryDisplayLauncher/res/values-mr/strings.xml
deleted file mode 100644
index 6e92a2f..0000000
--- a/SecondaryDisplayLauncher/res/values-mr/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"अ‍ॅक्टिव्हिटी लाँच करता आली नाही"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"अ‍ॅप शॉर्टकट जोडा"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"वॉलपेपर सेट करा"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ms/strings.xml b/SecondaryDisplayLauncher/res/values-ms/strings.xml
deleted file mode 100644
index fd78053..0000000
--- a/SecondaryDisplayLauncher/res/values-ms/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Tidak dapat melancarkan aktiviti"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Tambah pintasan apl"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Tetapkan kertas dinding"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-my/strings.xml b/SecondaryDisplayLauncher/res/values-my/strings.xml
deleted file mode 100644
index 1521402..0000000
--- a/SecondaryDisplayLauncher/res/values-my/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"လုပ်ဆောင်ချက်ကို စတင်၍မရပါ"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"အက်ပ်ဖြတ်လမ်းလင့်ခ်ထည့်ရန်"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"နောက်ခံ သတ်မှတ်ရန်"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-nb/strings.xml b/SecondaryDisplayLauncher/res/values-nb/strings.xml
deleted file mode 100644
index 945c87b..0000000
--- a/SecondaryDisplayLauncher/res/values-nb/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Kunne ikke starte aktiviteten"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Legg til en appsnarvei"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Angi bakgrunn"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ne/strings.xml b/SecondaryDisplayLauncher/res/values-ne/strings.xml
deleted file mode 100644
index 9a5b0a0..0000000
--- a/SecondaryDisplayLauncher/res/values-ne/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"उक्त क्रियाकलाप सुरु गर्न सकिएन"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"अनुप्रयोगको सर्टकट थप्नुहोस्‌"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"वालपेपर सेट गर्नुहोस्"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-nl/strings.xml b/SecondaryDisplayLauncher/res/values-nl/strings.xml
deleted file mode 100644
index 8767708..0000000
--- a/SecondaryDisplayLauncher/res/values-nl/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Kan de activiteit niet starten"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"App-snelkoppeling toevoegen"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Achtergrond instellen"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-or/strings.xml b/SecondaryDisplayLauncher/res/values-or/strings.xml
deleted file mode 100644
index 9bc5725..0000000
--- a/SecondaryDisplayLauncher/res/values-or/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ଗତିବିଧିକୁ ଲଞ୍ଚ କରାଯାଇପାରିଲା ନାହିଁ"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ଆପ୍‌ ସର୍ଟକଟ୍‌ ଯୋଗ କରନ୍ତୁ"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ୱାଲ୍‌‌ପେପର୍‌କୁ ସେଟ୍ କରନ୍ତୁ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-pa/strings.xml b/SecondaryDisplayLauncher/res/values-pa/strings.xml
deleted file mode 100644
index c5dd582..0000000
--- a/SecondaryDisplayLauncher/res/values-pa/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ਸਰਗਰਮੀ ਨੂੰ ਲਾਂਚ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ਐਪ ਸ਼ਾਰਟਕੱਟ ਸ਼ਾਮਲ ਕਰੋ"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ਵਾਲਪੇਪਰ ਸੈੱਟ ਕਰੋ"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-pl/strings.xml b/SecondaryDisplayLauncher/res/values-pl/strings.xml
deleted file mode 100644
index e8efaed..0000000
--- a/SecondaryDisplayLauncher/res/values-pl/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Nie udało się uruchomić aktywności"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Dodaj skrót do aplikacji"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Ustaw tapetę"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-pt-rPT/strings.xml b/SecondaryDisplayLauncher/res/values-pt-rPT/strings.xml
deleted file mode 100644
index 67c7557..0000000
--- a/SecondaryDisplayLauncher/res/values-pt-rPT/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Não foi possível iniciar a atividade."</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Adicionar atalho de aplicação"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Definir imagem de fundo"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-pt/strings.xml b/SecondaryDisplayLauncher/res/values-pt/strings.xml
deleted file mode 100644
index 201fc07..0000000
--- a/SecondaryDisplayLauncher/res/values-pt/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Não foi possível abrir a atividade"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Adicionar atalho de apps"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Definir plano de fundo"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ro/strings.xml b/SecondaryDisplayLauncher/res/values-ro/strings.xml
deleted file mode 100644
index e2e21c5..0000000
--- a/SecondaryDisplayLauncher/res/values-ro/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Nu s-a putut lansa activitatea"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Adăugați comanda rapidă pentru aplicație"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Setați imaginea de fundal"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ru/strings.xml b/SecondaryDisplayLauncher/res/values-ru/strings.xml
deleted file mode 100644
index 64ba00e..0000000
--- a/SecondaryDisplayLauncher/res/values-ru/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Не удалось запустить объект activity"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Добавить ярлык приложения"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Установить обои"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-si/strings.xml b/SecondaryDisplayLauncher/res/values-si/strings.xml
deleted file mode 100644
index ac492eb..0000000
--- a/SecondaryDisplayLauncher/res/values-si/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"ක්‍රියාකාරකම දියත් කිරීමට නොහැකි විය"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"යෙදුම් කෙටිමඟ එක් කරන්න"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"බිතුපත සකසන්න"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sk/strings.xml b/SecondaryDisplayLauncher/res/values-sk/strings.xml
deleted file mode 100644
index 5e6fa7a..0000000
--- a/SecondaryDisplayLauncher/res/values-sk/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Aktivitu sa nepodarilo spustiť"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Pridať odkaz do aplikácie"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Nastaviť tapetu"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sl/strings.xml b/SecondaryDisplayLauncher/res/values-sl/strings.xml
deleted file mode 100644
index f54dec9..0000000
--- a/SecondaryDisplayLauncher/res/values-sl/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Dejavnosti ni bilo mogoče zagnati"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Dodaj bližnjico do aplikacije"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Nastavi ozadje"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sq/strings.xml b/SecondaryDisplayLauncher/res/values-sq/strings.xml
deleted file mode 100644
index e626dd1..0000000
--- a/SecondaryDisplayLauncher/res/values-sq/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Aktiviteti nuk mund të hapej"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Shto shkurtoren e aplikacionit"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Cakto imazhin e sfondit"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sr/strings.xml b/SecondaryDisplayLauncher/res/values-sr/strings.xml
deleted file mode 100644
index 94214f1..0000000
--- a/SecondaryDisplayLauncher/res/values-sr/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Покретање активности није успело"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Додај пречицу за апликацију"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Подесите позадину"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sv/strings.xml b/SecondaryDisplayLauncher/res/values-sv/strings.xml
deleted file mode 100644
index 53e17ef..0000000
--- a/SecondaryDisplayLauncher/res/values-sv/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Det gick inte att starta aktiviteten"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Lägg till appgenväg"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Ange bakgrund"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sw/strings.xml b/SecondaryDisplayLauncher/res/values-sw/strings.xml
deleted file mode 100644
index 490561a..0000000
--- a/SecondaryDisplayLauncher/res/values-sw/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Imeshindwa kuanzisha shughuli"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Ongeza njia ya mkato ya programu"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Weka mandhari"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sw600dp/dimens.xml b/SecondaryDisplayLauncher/res/values-sw600dp/dimens.xml
deleted file mode 100644
index f33a8db..0000000
--- a/SecondaryDisplayLauncher/res/values-sw600dp/dimens.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-    <dimen name="app_picker_width">400dp</dimen>
-    <dimen name="app_picker_height">400dp</dimen>
-    <dimen name="app_picker_fab_margin">60dp</dimen>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-sw720dp/dimens.xml b/SecondaryDisplayLauncher/res/values-sw720dp/dimens.xml
deleted file mode 100644
index 524e52a..0000000
--- a/SecondaryDisplayLauncher/res/values-sw720dp/dimens.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-    <dimen name="app_picker_width">660dp</dimen>
-    <dimen name="app_picker_height">660dp</dimen>
-    <dimen name="app_picker_fab_margin">70dp</dimen>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ta/strings.xml b/SecondaryDisplayLauncher/res/values-ta/strings.xml
deleted file mode 100644
index 6bb054a..0000000
--- a/SecondaryDisplayLauncher/res/values-ta/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"நடவடிக்கையைத் துவக்க இயலவில்லை"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ஆப்ஸ் ஷார்ட்கட்டைச் சேர்"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"வால்பேப்பரை அமை"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-te/strings.xml b/SecondaryDisplayLauncher/res/values-te/strings.xml
deleted file mode 100644
index 3dd3c9b..0000000
--- a/SecondaryDisplayLauncher/res/values-te/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"కార్యకలాపాన్ని ప్రారంభించడం సాధ్యం కాలేదు"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"యాప్ షార్ట్‌కట్‌ని జోడించు"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"వాల్‌పేపర్‌ను సెట్ చేయండి"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-th/strings.xml b/SecondaryDisplayLauncher/res/values-th/strings.xml
deleted file mode 100644
index 6368950..0000000
--- a/SecondaryDisplayLauncher/res/values-th/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"เปิดกิจกรรมไม่ได้"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"เพิ่มทางลัดของแอป"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"ตั้งวอลเปเปอร์"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-tl/strings.xml b/SecondaryDisplayLauncher/res/values-tl/strings.xml
deleted file mode 100644
index 192e5c4..0000000
--- a/SecondaryDisplayLauncher/res/values-tl/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Hindi mailunsad ang aktibidad"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Magdagdag ng shortcut ng app"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Magtakda ng wallpaper"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-tr/strings.xml b/SecondaryDisplayLauncher/res/values-tr/strings.xml
deleted file mode 100644
index e7ed998..0000000
--- a/SecondaryDisplayLauncher/res/values-tr/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"İşlem başlatılamadı"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Uygulama kısayolu ekle"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Duvar kağıdı ayarla"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-uk/strings.xml b/SecondaryDisplayLauncher/res/values-uk/strings.xml
deleted file mode 100644
index e465995..0000000
--- a/SecondaryDisplayLauncher/res/values-uk/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Не вдалося запустити активність"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Розмістити ярлик додатка"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Вибрати фоновий малюнок"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-ur/strings.xml b/SecondaryDisplayLauncher/res/values-ur/strings.xml
deleted file mode 100644
index e4c8641..0000000
--- a/SecondaryDisplayLauncher/res/values-ur/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"سرگرمی کو شروع نہیں کیا جا سکا"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"ایپ شارٹ کٹ شامل کریں"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"وال پیپر سیٹ کریں"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-uz/strings.xml b/SecondaryDisplayLauncher/res/values-uz/strings.xml
deleted file mode 100644
index 585739d..0000000
--- a/SecondaryDisplayLauncher/res/values-uz/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Faollik ishga tushmadi"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Yorliq yaratish"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Fonga rasm oʻrnatish"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-vi/strings.xml b/SecondaryDisplayLauncher/res/values-vi/strings.xml
deleted file mode 100644
index 15a1a44..0000000
--- a/SecondaryDisplayLauncher/res/values-vi/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Không thể chạy hoạt động"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Thêm lối tắt ứng dụng"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Đặt hình nền"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-zh-rCN/strings.xml b/SecondaryDisplayLauncher/res/values-zh-rCN/strings.xml
deleted file mode 100644
index 3358499..0000000
--- a/SecondaryDisplayLauncher/res/values-zh-rCN/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"无法启动该操作组件"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"添加应用快捷方式"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"设置壁纸"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-zh-rHK/strings.xml b/SecondaryDisplayLauncher/res/values-zh-rHK/strings.xml
deleted file mode 100644
index bf76f29..0000000
--- a/SecondaryDisplayLauncher/res/values-zh-rHK/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"無法啟動活動"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"新增應用程式捷徑"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"設定桌布"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-zh-rTW/strings.xml b/SecondaryDisplayLauncher/res/values-zh-rTW/strings.xml
deleted file mode 100644
index c02fe2c..0000000
--- a/SecondaryDisplayLauncher/res/values-zh-rTW/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"無法啟動活動"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"新增應用程式捷徑"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"套用桌布"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values-zu/strings.xml b/SecondaryDisplayLauncher/res/values-zu/strings.xml
deleted file mode 100644
index ad2f6b9..0000000
--- a/SecondaryDisplayLauncher/res/values-zu/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch" msgid="7873588052226763866">"Ayikwazanga ukuqalisa umsebenzi"</string>
-    <string name="add_app_shortcut" msgid="2756755330707509435">"Engeza isinqamuleli sohlelo lokusebenza"</string>
-    <string name="set_wallpaper" msgid="6475195450505435904">"Setha isithombe sangemuva"</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values/colors.xml b/SecondaryDisplayLauncher/res/values/colors.xml
deleted file mode 100644
index 66b41a2..0000000
--- a/SecondaryDisplayLauncher/res/values/colors.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/any/colors.xml
-**
-** Copyright 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.
-*/
--->
-<resources>
-    <color name="launcher_bg_color">#884e8391</color>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values/dimens.xml b/SecondaryDisplayLauncher/res/values/dimens.xml
deleted file mode 100644
index 7cca607..0000000
--- a/SecondaryDisplayLauncher/res/values/dimens.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<resources>
-    <dimen name="app_list_col_width">72dp</dimen>
-    <dimen name="app_list_horizontal_spacing">24dp</dimen>
-    <dimen name="app_list_vertical_spacing">24dp</dimen>
-    <dimen name="app_icon_width">64dp</dimen>
-    <dimen name="app_icon_height">64dp</dimen>
-    <dimen name="app_grid_margin_top">24dp</dimen>
-    <dimen name="app_grid_margin_left">8dp</dimen>
-    <dimen name="app_grid_margin_right">8dp</dimen>
-
-    <dimen name="app_picker_width">300dp</dimen>
-    <dimen name="app_picker_height">300dp</dimen>
-    <dimen name="app_picker_fab_margin">20dp</dimen>
-    <dimen name="options_button_margin">20dp</dimen>
-</resources>
diff --git a/SecondaryDisplayLauncher/res/values/strings.xml b/SecondaryDisplayLauncher/res/values/strings.xml
deleted file mode 100644
index b68918a..0000000
--- a/SecondaryDisplayLauncher/res/values/strings.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2018 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
--->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="couldnt_launch">Couldn\'t launch the activity</string>
-    <string name="add_app_shortcut">Add app shortcut</string>
-    <string name="set_wallpaper">Set wallpaper</string>
-</resources>
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java
deleted file mode 100644
index 3017b81..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/AppEntry.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-
-/** An entry that represents a single activity that can be launched. */
-public  class AppEntry {
-
-    private String mLabel;
-    private Drawable mIcon;
-    private Intent mLaunchIntent;
-
-    AppEntry(ResolveInfo info, PackageManager packageManager) {
-        mLabel = info.loadLabel(packageManager).toString();
-        mIcon = info.loadIcon(packageManager);
-        mLaunchIntent = new Intent();
-        mLaunchIntent.setComponent(new ComponentName(info.activityInfo.packageName,
-                info.activityInfo.name));
-    }
-
-    String getLabel() {
-        return mLabel;
-    }
-
-    Drawable getIcon() {
-        return mIcon;
-    }
-
-    Intent getLaunchIntent() { return mLaunchIntent; }
-
-    ComponentName getComponentName() {
-        return mLaunchIntent.getComponent();
-    }
-
-    @Override
-    public String toString() {
-        return mLabel;
-    }
-}
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java
deleted file mode 100644
index aa115cb..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListAdapter.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.launcher3.R;
-
-import java.util.List;
-
-/** Adapter for available apps list. */
-public class AppListAdapter extends ArrayAdapter<AppEntry> {
-    private final LayoutInflater mInflater;
-
-    AppListAdapter(Context context) {
-        super(context, android.R.layout.simple_list_item_2);
-        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-    }
-
-    void setData(List<AppEntry> data) {
-        clear();
-        if (data != null) {
-            addAll(data);
-        }
-    }
-
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        View view;
-
-        if (convertView == null) {
-            view = mInflater.inflate(R.layout.app_grid_item, parent, false);
-        } else {
-            view = convertView;
-        }
-
-        AppEntry item = getItem(position);
-        ((ImageView)view.findViewById(R.id.app_icon)).setImageDrawable(item.getIcon());
-        ((TextView)view.findViewById(R.id.app_name)).setText(item.getLabel());
-
-        return view;
-    }
-}
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java b/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java
deleted file mode 100644
index 914fd5e..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/AppListViewModel.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.app.Application;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.AsyncTask;
-
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A view model that provides a list of activities that can be launched.
- */
-public class AppListViewModel extends AndroidViewModel {
-
-    private final AppListLiveData mLiveData;
-    private final PackageIntentReceiver
-            mPackageIntentReceiver;
-
-    public AppListViewModel(Application application) {
-        super(application);
-        mLiveData = new AppListLiveData(application);
-        mPackageIntentReceiver = new PackageIntentReceiver(mLiveData, application);
-    }
-
-    public LiveData<List<AppEntry>> getAppList() {
-        return mLiveData;
-    }
-
-    protected void onCleared() {
-        getApplication().unregisterReceiver(mPackageIntentReceiver);
-    }
-}
-
-class AppListLiveData extends LiveData<List<AppEntry>> {
-
-    private final PackageManager mPackageManager;
-    private int mCurrentDataVersion;
-
-    public AppListLiveData(Context context) {
-        mPackageManager = context.getPackageManager();
-        loadData();
-    }
-
-    void loadData() {
-        final int loadDataVersion = ++mCurrentDataVersion;
-
-        new AsyncTask<Void, Void, List<AppEntry>>() {
-            @Override
-            protected List<AppEntry> doInBackground(Void... voids) {
-                Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
-                mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
-
-                List<ResolveInfo> apps = mPackageManager.queryIntentActivities(mainIntent,
-                        PackageManager.GET_META_DATA);
-
-                List<AppEntry> entries = new ArrayList<>();
-                if (apps != null) {
-                    for (ResolveInfo app : apps) {
-                        AppEntry entry = new AppEntry(app, mPackageManager);
-                        entries.add(entry);
-                    }
-                }
-                return entries;
-            }
-
-            @Override
-            protected void onPostExecute(List<AppEntry> data) {
-                if (mCurrentDataVersion == loadDataVersion) {
-                    setValue(data);
-                }
-            }
-        }.execute();
-    }
-}
-
-/**
- * Receiver used to notify live data about app list changes.
- */
-class PackageIntentReceiver extends BroadcastReceiver {
-
-    private final AppListLiveData mLiveData;
-
-    public PackageIntentReceiver(AppListLiveData liveData, Context context) {
-        mLiveData = liveData;
-        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addDataScheme("package");
-        context.registerReceiver(this, filter);
-
-        // Register for events related to sdcard installation.
-        IntentFilter sdFilter = new IntentFilter();
-        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
-        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
-        context.registerReceiver(this, sdFilter);
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        mLiveData.loadData();
-    }
-}
\ No newline at end of file
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java
deleted file mode 100644
index 4f92038..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppListViewModel.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static com.android.launcher3.PinnedAppListViewModel.PINNED_APPS_KEY;
-
-import android.app.Application;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.AsyncTask;
-
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-/**
- * A view model that provides a list of activities that were pinned by user to always display on
- * home screen.
- * The pinned activities are stored in {@link SharedPreferences} to keep the sample simple :).
- */
-public class PinnedAppListViewModel extends AndroidViewModel {
-
-    final static String PINNED_APPS_KEY = "pinned_apps";
-
-    private final PinnedAppListLiveData mLiveData;
-
-    public PinnedAppListViewModel(Application application) {
-        super(application);
-        mLiveData = new PinnedAppListLiveData(application);
-    }
-
-    public LiveData<List<AppEntry>> getPinnedAppList() {
-        return mLiveData;
-    }
-}
-
-class PinnedAppListLiveData extends LiveData<List<AppEntry>> {
-
-    private final Context mContext;
-    private final PackageManager mPackageManager;
-    // Store listener reference, so it won't be GC-ed.
-    private final SharedPreferences.OnSharedPreferenceChangeListener mChangeListener;
-    private int mCurrentDataVersion;
-
-    public PinnedAppListLiveData(Context context) {
-        mContext = context;
-        mPackageManager = context.getPackageManager();
-
-        final SharedPreferences prefs = context.getSharedPreferences(PINNED_APPS_KEY, 0);
-        mChangeListener = (preferences, key) -> {
-            loadData();
-        };
-        prefs.registerOnSharedPreferenceChangeListener(mChangeListener);
-
-        loadData();
-    }
-
-    private void loadData() {
-        final int loadDataVersion = ++mCurrentDataVersion;
-
-        new AsyncTask<Void, Void, List<AppEntry>>() {
-            @Override
-            protected List<AppEntry> doInBackground(Void... voids) {
-                List<AppEntry> entries = new ArrayList<>();
-
-                final SharedPreferences sp = mContext.getSharedPreferences(PINNED_APPS_KEY, 0);
-                final Set<String> pinnedAppsComponents = sp.getStringSet(PINNED_APPS_KEY, null);
-                if (pinnedAppsComponents == null) {
-                    return null;
-                }
-
-                for (String componentString : pinnedAppsComponents) {
-                    final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
-                    mainIntent.setComponent(ComponentName.unflattenFromString(componentString));
-                    mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
-
-                    final List<ResolveInfo> apps = mPackageManager.queryIntentActivities(mainIntent,
-                            PackageManager.GET_META_DATA);
-
-                    if (apps != null) {
-                        for (ResolveInfo app : apps) {
-                            final AppEntry entry = new AppEntry(app, mPackageManager);
-                            entries.add(entry);
-                        }
-                    }
-                }
-
-                return entries;
-            }
-
-            @Override
-            protected void onPostExecute(List<AppEntry> data) {
-                if (mCurrentDataVersion == loadDataVersion) {
-                    setValue(data);
-                }
-            }
-        }.execute();
-    }
-}
\ No newline at end of file
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java b/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java
deleted file mode 100644
index 02e6e4a..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/PinnedAppPickerDialog.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.GridView;
-
-import androidx.fragment.app.DialogFragment;
-
-import com.android.launcher3.R;
-
-/**
- * Callback to be invoked when an app was picked.
- */
-interface AppPickedCallback {
-    void onAppPicked(AppEntry appEntry);
-}
-
-/**
- * Dialog that provides the user with a list of available apps to pin to the home screen.
- */
-public class PinnedAppPickerDialog extends DialogFragment {
-
-    private AppListAdapter mAppListAdapter;
-    private AppPickedCallback mAppPickerCallback;
-
-    public PinnedAppPickerDialog() {
-    }
-
-    public static PinnedAppPickerDialog newInstance(AppListAdapter appListAdapter,
-            AppPickedCallback callback) {
-        PinnedAppPickerDialog
-                frag = new PinnedAppPickerDialog();
-        frag.mAppListAdapter = appListAdapter;
-        frag.mAppPickerCallback = callback;
-        return frag;
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.app_picker_dialog, container);
-    }
-
-    @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-
-        GridView appGridView = view.findViewById(R.id.picker_app_grid);
-        appGridView.setAdapter(mAppListAdapter);
-        appGridView.setOnItemClickListener((adapterView, itemView, position, id) -> {
-            final AppEntry entry = mAppListAdapter.getItem(position);
-            mAppPickerCallback.onAppPicked(entry);
-            dismiss();
-        });
-    }
-}
\ No newline at end of file
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java b/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java
deleted file mode 100644
index 0a2f18f..0000000
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/**
- * Copyright (c) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static com.android.launcher3.PinnedAppListViewModel.PINNED_APPS_KEY;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.app.AlertDialog;
-import android.app.Application;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.GridView;
-import android.widget.ImageButton;
-import android.widget.PopupMenu;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
-
-import com.google.android.material.circularreveal.cardview.CircularRevealCardView;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Secondary launcher activity. It's launch mode is configured as "singleTop" to allow showing on
- * multiple displays and to ensure a single instance per each display.
- */
-public class SecondaryDisplayLauncher extends FragmentActivity implements AppPickedCallback,
-        PopupMenu.OnMenuItemClickListener {
-
-    private AppListAdapter mAppListAdapter;
-    private AppListAdapter mPinnedAppListAdapter;
-    private CircularRevealCardView mAppDrawerView;
-    private FloatingActionButton mFab;
-
-    private boolean mAppDrawerShown;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.secondary_display_launcher);
-
-        mAppDrawerView = findViewById(R.id.FloatingSheet);
-        mFab = findViewById(R.id.FloatingActionButton);
-
-        mFab.setOnClickListener((View v) -> {
-            showAppDrawer(true);
-        });
-
-        final ViewModelProvider viewModelProvider = new ViewModelProvider(getViewModelStore(),
-                new AndroidViewModelFactory((Application) getApplicationContext()));
-
-        mPinnedAppListAdapter = new AppListAdapter(this);
-        final GridView pinnedAppGridView = findViewById(R.id.pinned_app_grid);
-        pinnedAppGridView.setAdapter(mPinnedAppListAdapter);
-        pinnedAppGridView.setOnItemClickListener((adapterView, view, position, id) -> {
-            final AppEntry entry = mPinnedAppListAdapter.getItem(position);
-            launch(entry.getLaunchIntent());
-        });
-        final PinnedAppListViewModel pinnedAppListViewModel =
-                viewModelProvider.get(PinnedAppListViewModel.class);
-        pinnedAppListViewModel.getPinnedAppList().observe(this, data -> {
-            mPinnedAppListAdapter.setData(data);
-        });
-
-        mAppListAdapter = new AppListAdapter(this);
-        final GridView appGridView = findViewById(R.id.app_grid);
-        appGridView.setAdapter(mAppListAdapter);
-        appGridView.setOnItemClickListener((adapterView, view, position, id) -> {
-            final AppEntry entry = mAppListAdapter.getItem(position);
-            launch(entry.getLaunchIntent());
-        });
-        final AppListViewModel appListViewModel = viewModelProvider.get(AppListViewModel.class);
-        appListViewModel.getAppList().observe(this, data -> {
-            mAppListAdapter.setData(data);
-        });
-
-        ImageButton optionsButton = findViewById(R.id.OptionsButton);
-        optionsButton.setOnClickListener((View v) -> {
-            PopupMenu popup = new PopupMenu(this,v);
-            popup.setOnMenuItemClickListener(this);
-            MenuInflater inflater = popup.getMenuInflater();
-            inflater.inflate(R.menu.context_menu, popup.getMenu());
-            popup.show();
-        });
-    }
-
-    @Override
-    public boolean onMenuItemClick(MenuItem item) {
-        // Respond to picking one of the popup menu items.
-        final int id = item.getItemId();
-        if (id == R.id.add_app_shortcut) {
-            FragmentManager fm = getSupportFragmentManager();
-            PinnedAppPickerDialog pickerDialogFragment =
-                    PinnedAppPickerDialog.newInstance(mAppListAdapter, this);
-            pickerDialogFragment.show(fm, "fragment_app_picker");
-            return true;
-        } else if (id == R.id.set_wallpaper) {
-            Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
-            startActivity(Intent.createChooser(intent, getString(R.string.set_wallpaper)));
-            return true;
-        }
-
-        return true;
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        showAppDrawer(false);
-    }
-
-    public void onBackPressed() {
-        // If the app drawer was shown - hide it. Otherwise, not doing anything since we don't want
-        // to close the launcher.
-        showAppDrawer(false);
-    }
-
-    public void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-
-        if (Intent.ACTION_MAIN.equals(intent.getAction())) {
-            // Hide keyboard.
-            final View v = getWindow().peekDecorView();
-            if (v != null && v.getWindowToken() != null) {
-                getSystemService(InputMethodManager.class).hideSoftInputFromWindow(
-                        v.getWindowToken(), 0);
-            }
-        }
-
-        // A new intent will bring the launcher to top. Hide the app drawer to reset the state.
-        showAppDrawer(false);
-    }
-
-    void launch(Intent launchIntent) {
-        launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        try {
-            startActivity(launchIntent);
-        } catch (Exception e) {
-            final AlertDialog.Builder builder =
-                    new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert);
-            builder.setTitle(R.string.couldnt_launch)
-                    .setMessage(e.getLocalizedMessage())
-                    .setIcon(android.R.drawable.ic_dialog_alert)
-                    .show();
-        }
-    }
-
-    /**
-     * Store the picked app to persistent pinned list and update the loader.
-     */
-    @Override
-    public void onAppPicked(AppEntry appEntry) {
-        final SharedPreferences sp = getSharedPreferences(PINNED_APPS_KEY, 0);
-        Set<String> pinnedApps = sp.getStringSet(PINNED_APPS_KEY, null);
-        if (pinnedApps == null) {
-            pinnedApps = new HashSet<String>();
-        } else {
-            // Always need to create a new object to make sure that the changes are persisted.
-            pinnedApps = new HashSet<String>(pinnedApps);
-        }
-        pinnedApps.add(appEntry.getComponentName().flattenToString());
-
-        final SharedPreferences.Editor editor = sp.edit();
-        editor.putStringSet(PINNED_APPS_KEY, pinnedApps);
-        editor.apply();
-    }
-
-    /**
-     * Show/hide app drawer card with animation.
-     */
-    private void showAppDrawer(boolean show) {
-        if (show == mAppDrawerShown) {
-            return;
-        }
-
-        final Animator animator = revealAnimator(mAppDrawerView, show);
-        if (show) {
-            mAppDrawerShown = true;
-            mAppDrawerView.setVisibility(View.VISIBLE);
-            mFab.setVisibility(View.INVISIBLE);
-        } else {
-            mAppDrawerShown = false;
-            animator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    mAppDrawerView.setVisibility(View.INVISIBLE);
-                    mFab.setVisibility(View.VISIBLE);
-                }
-            });
-        }
-        animator.start();
-    }
-
-    /**
-     * Create reveal/hide animator for app list card.
-     */
-    private Animator revealAnimator(View view, boolean open) {
-        final int radius = (int) Math.hypot((double) view.getWidth(), (double) view.getHeight());
-        return ViewAnimationUtils.createCircularReveal(view, view.getRight(), view.getBottom(),
-                open ? 0 : radius, open ? radius : 0);
-    }
-}
diff --git a/SharedLibWrapper/build.gradle b/SharedLibWrapper/build.gradle
new file mode 100644
index 0000000..674e38a
--- /dev/null
+++ b/SharedLibWrapper/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'java'
+
+final String ANDROID_TOP = "${rootDir}/../../.."
+final String FRAMEWORK_PREBUILTS_DIR = "${ANDROID_TOP}/prebuilts/framework_intermediates/"
+
+sourceSets {
+    main {
+        java.srcDirs = ["${ANDROID_TOP}/frameworks/lib/systemui/SharedLibWrapper/src"]
+    }
+}
+
+sourceCompatibility = 1.8
+
+dependencies {
+    implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
+    compileOnly fileTree(dir: "$ANDROID_TOP/prebuilts/fullsdk-${org.gradle.internal.os.OperatingSystem.current().isMacOsX() ? "darwin" : "linux"}/platforms/${COMPILE_SDK}", include: 'android.jar')
+}
diff --git a/build.gradle b/build.gradle
index e296455..534ca65 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@
     repositories {
         mavenCentral()
         google()
+        jcenter()
     }
     dependencies {
         classpath GRADLE_CLASS_PATH
@@ -62,12 +63,6 @@
             minSdkVersion 28
         }
 
-        withQuickstepIconRecents {
-            dimension "recents"
-
-            minSdkVersion 28
-        }
-
         withoutQuickstep {
             dimension "recents"
         }
@@ -78,11 +73,6 @@
         if (variant.buildType.name.endsWith('release')) {
             variant.setIgnore(true)
         }
-
-        // Icon recents is Go only
-        if (name.contains("WithQuickstepIconRecents") && !name.contains("l3go")) {
-            variant.setIgnore(true)
-        }
     }
 
     sourceSets {
@@ -96,10 +86,6 @@
             }
         }
 
-        debug {
-            manifest.srcFile "AndroidManifest.xml"
-        }
-
         androidTest {
             res.srcDirs = ['tests/res']
             java.srcDirs = ['tests/src', 'tests/tapl']
@@ -112,15 +98,30 @@
 
         aosp {
             java.srcDirs = ['src_flags', 'src_shortcuts_overrides']
+        }
+
+        aospWithoutQuickstep {
             manifest.srcFile "AndroidManifest.xml"
         }
 
+        aospWithQuickstep {
+            manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+        }
+
         l3go {
             res.srcDirs = ['go/res']
             java.srcDirs = ['go/src']
             manifest.srcFile "go/AndroidManifest.xml"
         }
 
+        l3goWithoutQuickstepDebug {
+            manifest.srcFile "AndroidManifest.xml"
+        }
+
+        l3goWithQuickstepDebug {
+            manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+        }
+
         withoutQuickstep {
             java.srcDirs = ['src_ui_overrides']
         }
@@ -130,20 +131,17 @@
             java.srcDirs = ['quickstep/src', 'quickstep/recents_ui_overrides/src']
             manifest.srcFile "quickstep/AndroidManifest.xml"
         }
-
-        withQuickstepIconRecents {
-            res.srcDirs = ['quickstep/res', 'go/quickstep/res']
-            java.srcDirs = ['quickstep/src', 'go/quickstep/src']
-            manifest.srcFile "quickstep/AndroidManifest.xml"
-        }
     }
 }
 
-repositories {
-    maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
-    maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
-    mavenCentral()
-    google()
+allprojects {
+    repositories {
+        maven { url "../../../prebuilts/sdk/current/androidx/m2repository" }
+        maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
+        maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
+        mavenCentral()
+        google()
+    }
 }
 
 dependencies {
@@ -151,14 +149,12 @@
     implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
     implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
     implementation project(':IconLoader')
+    withQuickstepImplementation project(':SharedLibWrapper')
     implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'launcher_protos.jar')
 
     // Recents lib dependency
     withQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
 
-    // Recents lib dependency for Go
-    withQuickstepIconRecentsImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
-
     // Required for AOSP to compile. This is already included in the sysui_shared.jar
     withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'plugin_core.jar')
 
@@ -175,7 +171,7 @@
 protobuf {
     // Configure the protoc executable
     protoc {
-        artifact = 'com.google.protobuf:protoc:3.0.0-alpha-3'
+        artifact = 'com.google.protobuf:protoc:3.0.0'
 
         generateProtoTasks {
             all().each { task ->
diff --git a/ext_tests/res/values/overrides.xml b/ext_tests/res/values/overrides.xml
new file mode 100644
index 0000000..3f071d4
--- /dev/null
+++ b/ext_tests/res/values/overrides.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="test_information_handler_class" translatable="false">com.android.launcher3.testing.DebugTestInformationHandler</string>
+</resources>
+
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
new file mode 100644
index 0000000..e649ce1
--- /dev/null
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.testing;
+
+import static android.graphics.Bitmap.Config.ARGB_8888;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Debug;
+import android.system.Os;
+import android.view.View;
+
+import androidx.annotation.Keep;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class to handle requests from tests, including debug ones.
+ */
+public class DebugTestInformationHandler extends TestInformationHandler {
+    private static LinkedList sLeaks;
+    private static Collection<String> sEvents;
+
+    public DebugTestInformationHandler(Context context) {
+        init(context);
+    }
+
+    private static void runGcAndFinalizersSync() {
+        Runtime.getRuntime().gc();
+        Runtime.getRuntime().runFinalization();
+
+        final CountDownLatch fence = new CountDownLatch(1);
+        createFinalizationObserver(fence);
+        try {
+            do {
+                Runtime.getRuntime().gc();
+                Runtime.getRuntime().runFinalization();
+            } while (!fence.await(100, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    // Create the observer in the scope of a method to minimize the chance that
+    // it remains live in a DEX/machine register at the point of the fence guard.
+    // This must be kept to avoid R8 inlining it.
+    @Keep
+    private static void createFinalizationObserver(CountDownLatch fence) {
+        new Object() {
+            @Override
+            protected void finalize() throws Throwable {
+                try {
+                    fence.countDown();
+                } finally {
+                    super.finalize();
+                }
+            }
+        };
+    }
+
+    @Override
+    public Bundle call(String method) {
+        final Bundle response = new Bundle();
+        switch (method) {
+            case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> l.getAppsView().getAppsStore().getDeferUpdatesFlags());
+            }
+
+            case TestProtocol.REQUEST_ENABLE_DEBUG_TRACING:
+                TestProtocol.sDebugTracing = true;
+                return response;
+
+            case TestProtocol.REQUEST_DISABLE_DEBUG_TRACING:
+                TestProtocol.sDebugTracing = false;
+                return response;
+
+            case TestProtocol.REQUEST_PID: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
+                return response;
+            }
+
+            case TestProtocol.REQUEST_TOTAL_PSS_KB: {
+                runGcAndFinalizersSync();
+                Debug.MemoryInfo mem = new Debug.MemoryInfo();
+                Debug.getMemoryInfo(mem);
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mem.getTotalPss());
+                return response;
+            }
+
+            case TestProtocol.REQUEST_JAVA_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+
+                // Allocate and dirty the memory.
+                final int leakSize = 1024 * 1024;
+                final byte[] bytes = new byte[leakSize];
+                for (int i = 0; i < leakSize; i += 239) {
+                    bytes[i] = (byte) (i % 256);
+                }
+                sLeaks.add(bytes);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_NATIVE_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+
+                // Allocate and dirty a bitmap.
+                final Bitmap bitmap = Bitmap.createBitmap(512, 512, ARGB_8888);
+                bitmap.eraseColor(Color.RED);
+                sLeaks.add(bitmap);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_VIEW_LEAK: {
+                if (sLeaks == null) sLeaks = new LinkedList();
+                sLeaks.add(new View(mContext));
+                return response;
+            }
+
+            case TestProtocol.REQUEST_START_EVENT_LOGGING: {
+                sEvents = new ArrayList<>();
+                TestLogging.setEventConsumer(
+                        (sequence, event) -> {
+                            final Collection<String> events = sEvents;
+                            if (events != null) {
+                                synchronized (events) {
+                                    events.add(sequence + '/' + event);
+                                }
+                            }
+                        });
+                return response;
+            }
+
+            case TestProtocol.REQUEST_STOP_EVENT_LOGGING: {
+                TestLogging.setEventConsumer(null);
+                sEvents = null;
+                return response;
+            }
+
+            case TestProtocol.REQUEST_GET_TEST_EVENTS: {
+                synchronized (sEvents) {
+                    response.putStringArrayList(
+                            TestProtocol.TEST_INFO_RESPONSE_FIELD, new ArrayList<>(sEvents));
+                }
+                return response;
+            }
+
+            case TestProtocol.REQUEST_CLEAR_DATA: {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    LauncherSettings.Settings.call(mContext.getContentResolver(),
+                            LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+                    MAIN_EXECUTOR.submit(() ->
+                            LauncherAppState.getInstance(mContext).getModel().forceReload());
+                    return response;
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+
+            default:
+                return super.call(method);
+        }
+    }
+}
diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml
index fae1eff..f84a82e 100644
--- a/go/AndroidManifest.xml
+++ b/go/AndroidManifest.xml
@@ -22,7 +22,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3" >
 
-    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="25"/>
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
 
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/go/quickstep/res/drawable/default_thumbnail.xml b/go/quickstep/res/drawable/default_thumbnail.xml
deleted file mode 100644
index ab22dcf..0000000
--- a/go/quickstep/res/drawable/default_thumbnail.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/darker_gray"/>
-    <corners android:radius="@dimen/task_thumbnail_corner_radius"/>
-</shape>
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
deleted file mode 100644
index eef66ad..0000000
--- a/go/quickstep/res/layout/clear_all_button.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/clear_all_item_view"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/clear_all_item_view_height">
-    <Button
-        android:id="@+id/clear_all_button"
-        android:layout_width="@dimen/clear_all_button_width"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_horizontal"
-        android:background="@drawable/clear_all_button"
-        android:gravity="center"
-        android:text="@string/recents_clear_all"
-        android:textAllCaps="false"
-        android:textColor="@color/clear_all_button_text"
-        android:textSize="14sp"
-        style="@style/TextTitle"/>
-</FrameLayout>
diff --git a/go/quickstep/res/layout/fallback_recents_activity.xml b/go/quickstep/res/layout/fallback_recents_activity.xml
deleted file mode 100644
index 653f463..0000000
--- a/go/quickstep/res/layout/fallback_recents_activity.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.quickstep.fallback.GoRecentsActivityRootView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/drag_layer"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
-
-    <include
-        android:id="@+id/overview_panel"
-        layout="@layout/overview_panel"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:outlineProvider="none"
-        android:theme="@style/HomeScreenElementTheme" />
-</com.android.quickstep.fallback.GoRecentsActivityRootView>
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
deleted file mode 100644
index 8381ebc..0000000
--- a/go/quickstep/res/layout/icon_recents_root_view.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.quickstep.views.IconRecentsView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:clipChildren="false">
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/recent_task_recycler_view"
-        android:layout_width="@dimen/recents_list_width"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_horizontal"
-        android:scrollbars="none"
-        android:clipToPadding="false"
-        android:clipChildren="false"/>
-    <TextView
-        android:id="@+id/recent_task_empty_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:gravity="center"
-        android:text="@string/recents_empty_message"
-        android:textColor="@android:color/white"
-        android:textSize="25sp"
-        style="@style/TextTitle"
-        android:visibility="gone"/>
-</com.android.quickstep.views.IconRecentsView>
\ No newline at end of file
diff --git a/go/quickstep/res/layout/overview_panel.xml b/go/quickstep/res/layout/overview_panel.xml
deleted file mode 100644
index 601edce..0000000
--- a/go/quickstep/res/layout/overview_panel.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<fragment android:name="com.android.quickstep.IconRecentsFragment"
-          xmlns:android="http://schemas.android.com/apk/res/android"
-          android:id="@+id/low_ram_recents_fragment"
-          android:layout_width="match_parent"
-          android:layout_height="match_parent"/>
diff --git a/go/quickstep/res/layout/task_item_view.xml b/go/quickstep/res/layout/task_item_view.xml
deleted file mode 100644
index aeac477..0000000
--- a/go/quickstep/res/layout/task_item_view.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.quickstep.views.TaskItemView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/task_item_height"
-    android:orientation="horizontal"
-    android:clipChildren="false">
-    <com.android.quickstep.views.TaskThumbnailIconView
-        android:id="@+id/task_icon_and_thumbnail"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginHorizontal="@dimen/task_thumbnail_icon_horiz_margin">
-        <ImageView
-            android:id="@+id/task_thumbnail"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-        <ImageView
-            android:id="@+id/task_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-    </com.android.quickstep.views.TaskThumbnailIconView>
-    <TextView
-        android:id="@+id/task_label"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:singleLine="true"
-        android:textColor="@android:color/white"
-        android:textSize="24sp"
-        style="@style/TextTitle"/>
-</com.android.quickstep.views.TaskItemView>
diff --git a/go/quickstep/res/values-sw480dp/dimens.xml b/go/quickstep/res/values-sw480dp/dimens.xml
deleted file mode 100644
index 571b8a1..0000000
--- a/go/quickstep/res/values-sw480dp/dimens.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources>
-    <dimen name="recents_list_width">480dp</dimen>
-
-    <dimen name="task_item_height">90dp</dimen>
-    <dimen name="task_item_top_margin">24dp</dimen>
-    <dimen name="task_thumbnail_icon_horiz_margin">24dp</dimen>
-
-    <dimen name="task_thumbnail_corner_radius">4dp</dimen>
-
-    <dimen name="clear_all_item_view_height">52dp</dimen>
-    <dimen name="clear_all_item_view_top_margin">28dp</dimen>
-    <dimen name="clear_all_item_view_bottom_margin">28dp</dimen>
-    <dimen name="clear_all_button_width">160dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/values/dimens.xml
deleted file mode 100644
index 91040f2..0000000
--- a/go/quickstep/res/values/dimens.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources>
-    <dimen name="recents_list_width">320dp</dimen>
-
-    <dimen name="task_item_height">60dp</dimen>
-    <dimen name="task_item_top_margin">16dp</dimen>
-    <dimen name="task_thumbnail_icon_horiz_margin">16dp</dimen>
-
-    <dimen name="task_thumbnail_corner_radius">3dp</dimen>
-
-    <dimen name="clear_all_item_view_height">36dp</dimen>
-    <dimen name="clear_all_item_view_top_margin">20dp</dimen>
-    <dimen name="clear_all_item_view_bottom_margin">20dp</dimen>
-    <dimen name="clear_all_button_width">106dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/override.xml b/go/quickstep/res/values/override.xml
deleted file mode 100644
index bb267a3..0000000
--- a/go/quickstep/res/values/override.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- Class overrides for Go version of launcher with Go recents. -->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="app_transition_manager_class" translatable="false">com.android.launcher3.GoLauncherAppTransitionManagerImpl</string>
-
-  <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
-
-  <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
-
-  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
-</resources>
-
diff --git a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java b/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
deleted file mode 100644
index bcb1f5c..0000000
--- a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.android.launcher3;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.os.Handler;
-import android.view.View;
-
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * A {@link QuickstepAppTransitionManagerImpl} with recents-specific app transitions based off
- * {@link com.android.quickstep.views.IconRecentsView}.
- */
-public final class GoLauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
-
-    public GoLauncherAppTransitionManagerImpl(Context context) {
-        super(context);
-    }
-
-    @Override
-    protected boolean isLaunchingFromRecents(View v, RemoteAnimationTargetCompat[] targets) {
-        return mLauncher.getStateManager().getState().overviewUi;
-    }
-
-    @Override
-    RemoteAnimationRunnerCompat getWallpaperOpenRunner(boolean fromUnlock) {
-        return new GoWallpaperOpenLauncherAnimationRunner(mHandler,
-                false /* startAtFrontOfQueue */, fromUnlock);
-    }
-
-    @Override
-    protected void composeRecentsLaunchAnimator(AnimatorSet anim, View v,
-            RemoteAnimationTargetCompat[] targets, boolean launcherClosing) {
-        // Stubbed. Recents launch animation will come from the recents view itself and will not
-        // use remote animations.
-    }
-
-    @Override
-    protected Runnable composeViewContentAnimator(AnimatorSet anim, float[] alphas, float[] trans) {
-        IconRecentsView overview = mLauncher.getOverviewPanel();
-        ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
-                CONTENT_ALPHA, alphas);
-        alpha.setDuration(CONTENT_ALPHA_DURATION);
-        alpha.setInterpolator(LINEAR);
-        anim.play(alpha);
-
-        ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
-        transY.setInterpolator(AGGRESSIVE_EASE);
-        transY.setDuration(CONTENT_TRANSLATION_DURATION);
-        anim.play(transY);
-
-        return mLauncher.getStateManager()::reapplyState;
-    }
-
-    /**
-     * Remote animation runner for animation from app to Launcher. For Go, when going to recents,
-     * we need to ensure that the recents view is ready for remote animation before starting.
-     */
-    private final class GoWallpaperOpenLauncherAnimationRunner extends
-            WallpaperOpenLauncherAnimationRunner {
-        public GoWallpaperOpenLauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue,
-                boolean fromUnlock) {
-            super(handler, startAtFrontOfQueue, fromUnlock);
-        }
-
-        @Override
-        public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
-                AnimationResult result) {
-            boolean isGoingToRecents =
-                    taskIsATargetWithMode(targetCompats, mLauncher.getTaskId(), MODE_OPENING)
-                    && (mLauncher.getStateManager().getState() == LauncherState.OVERVIEW);
-            if (isGoingToRecents) {
-                IconRecentsView recentsView = mLauncher.getOverviewPanel();
-                if (!recentsView.isReadyForRemoteAnim()) {
-                    recentsView.setOnReadyForRemoteAnimCallback(() ->
-                        postAsyncCallback(mHandler, () -> onCreateAnimation(targetCompats, result))
-                    );
-                    return;
-                }
-            }
-            super.onCreateAnimation(targetCompats, result);
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java b/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java
deleted file mode 100644
index c12da94..0000000
--- a/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import com.android.quickstep.RecentsToActivityHelper;
-
-/**
- * {@link RecentsToActivityHelper} for when the recents implementation is contained in
- * {@link Launcher}.
- */
-public final class LauncherRecentsToActivityHelper implements RecentsToActivityHelper {
-
-    private final Launcher mLauncher;
-
-    public LauncherRecentsToActivityHelper(Launcher launcher) {
-        mLauncher = launcher;
-    }
-
-    @Override
-    public void leaveRecents() {
-        mLauncher.getStateManager().goToState(NORMAL);
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
deleted file mode 100644
index cbc77d2..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.ArrayList;
-
-/**
- * Provides recents-related {@link UiFactory} logic and classes.
- */
-public abstract class RecentsUiFactory {
-
-    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = true;
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(launcher.getDragController());
-
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new LandscapeStatesTouchController(launcher));
-            list.add(new LandscapeEdgeSwipeController(launcher));
-        } else {
-            boolean allowDragToOverview = SysUINavigationMode.INSTANCE.get(launcher)
-                    .getMode().hasGestures;
-            list.add(new PortraitStatesTouchController(launcher, allowDragToOverview));
-        }
-        if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE
-                && !launcher.getDeviceProfile().isMultiWindowMode
-                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new StatusBarTouchController(launcher));
-        }
-        return list.toArray(new TouchController[list.size()]);
-    }
-
-    /**
-     * Creates and returns the controller responsible for recents view state transitions.
-     *
-     * @param launcher the launcher activity
-     * @return state handler for recents
-     */
-    public static StateHandler createRecentsViewStateController(Launcher launcher) {
-        return new RecentsViewStateController(launcher);
-    }
-
-    /**
-     * Clean-up logic that occurs when recents is no longer in use/visible.
-     *
-     * @param launcher the launcher activity
-     */
-    public static void resetOverview(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.setTransitionedFromApp(false);
-    }
-
-    /**
-     * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
-     *
-     * @param launcher the launcher activity
-     */
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) {}
-
-    public static RotationMode getRotationMode(DeviceProfile dp) {
-        return RotationMode.NORMAL;
-    }
-
-    public static void clearSwipeSharedState(boolean finishAnimation) {}
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
deleted file mode 100644
index 0b12ab0..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-
-import android.util.FloatProperty;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherRecentsToActivityHelper;
-import com.android.quickstep.views.IconRecentsView;
-
-import androidx.annotation.NonNull;
-
-/**
- * State handler for Go's {@link IconRecentsView}.
- */
-public final class RecentsViewStateController extends
-        BaseRecentsViewStateController<IconRecentsView> {
-
-    public RecentsViewStateController(@NonNull Launcher launcher) {
-        super(launcher);
-        launcher.<IconRecentsView>getOverviewPanel().setRecentsToActivityHelper(
-                new LauncherRecentsToActivityHelper(launcher));
-    }
-
-    @Override
-    FloatProperty<IconRecentsView> getContentAlphaProperty() {
-        return CONTENT_ALPHA;
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
deleted file mode 100644
index 212ce9b..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.states;
-
-import static android.view.View.VISIBLE;
-
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
-import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
-
-import android.content.Context;
-import android.view.View;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.views.IconRecentsView;
-
-/**
- * Definition for overview state
- */
-public class OverviewState extends LauncherState {
-
-    // Scale recents takes before animating in
-    private static final float RECENTS_PREPARE_SCALE = 1.33f;
-
-    private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
-            | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY;
-
-    public OverviewState(int id) {
-        this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
-    }
-
-    protected OverviewState(int id, int transitionDuration, int stateFlags) {
-        super(id, LauncherLogProto.ContainerType.TASKSWITCHER, transitionDuration, stateFlags);
-    }
-
-    @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1f, 0f, 0f);
-    }
-
-    @Override
-    public void onStateEnabled(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.onBeginTransitionToOverview();
-        recentsView.setShowStatusBarForegroundScrim(true);
-        // Request orientation be set to unspecified, letting the system decide the best
-        // orientation.
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
-    }
-
-    @Override
-    public void onStateDisabled(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.setShowStatusBarForegroundScrim(false);
-    }
-
-    @Override
-    public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        return new PageAlphaProvider(DEACCEL_2) {
-            @Override
-            public float getPageAlpha(int pageIndex) {
-                return 0;
-            }
-        };
-    }
-
-    @Override
-    public int getVisibleElements(Launcher launcher) {
-        return NONE;
-    }
-
-    @Override
-    public float getWorkspaceScrimAlpha(Launcher launcher) {
-        return 0.5f;
-    }
-
-    @Override
-    public String getDescription(Launcher launcher) {
-        return launcher.getString(R.string.accessibility_desc_recent_apps);
-    }
-
-    @Override
-    public void onBackPressed(Launcher launcher) {
-        // TODO: Add logic to go back to task if coming from a currently running task.
-        super.onBackPressed(launcher);
-    }
-
-    public static float getDefaultSwipeHeight(Launcher launcher) {
-        return getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
-    }
-
-    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
-        return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
-    }
-
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            AnimatorSetBuilder builder) {
-        if (fromState == NORMAL && this == OVERVIEW) {
-            if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL);
-                builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
-            } else {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
-            }
-            builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
-
-            View overview = launcher.getOverviewPanel();
-            if (overview.getVisibility() != VISIBLE) {
-                SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
-            }
-        }
-    }
-
-    public static OverviewState newBackgroundState(int id) {
-        return new OverviewState(id);
-    }
-
-    public static OverviewState newPeekState(int id) {
-        return new OverviewState(id);
-    }
-
-    public static OverviewState newSwitchState(int id) {
-        return new OverviewState(id);
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java b/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java
deleted file mode 100644
index 66aec40..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-/**
- * Touch controller for landscape mode.
- */
-public final class LandscapeStatesTouchController extends PortraitStatesTouchController {
-
-    public LandscapeStatesTouchController(Launcher l) {
-        super(l, true /* allowDragToOverview */);
-    }
-
-    @Override
-    protected boolean canInterceptTouch(MotionEvent ev) {
-        if (mCurrentAnimation != null) {
-            // If we are already animating from a previous state, we can intercept.
-            return true;
-        }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
-            return false;
-        }
-        if (mLauncher.isInState(ALL_APPS)) {
-            // In all-apps only listen if the container cannot scroll itself
-            return mLauncher.getAppsView().shouldContainerScroll(ev);
-        } else if (mLauncher.isInState(NORMAL)) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    @Override
-    protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (fromState == ALL_APPS && !isDragTowardPositive) {
-            return NORMAL;
-        } else if (isDragTowardPositive) {
-            return ALL_APPS;
-        }
-        return fromState;
-    }
-
-    @Override
-    protected int getLogContainerTypeForNormalState(MotionEvent ev) {
-        return LauncherLogProto.ContainerType.WORKSPACE;
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
deleted file mode 100644
index 011a4e7..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.PendingAnimation;
-
-/**
- * Helper class for {@link PortraitStatesTouchController} that determines swipeable regions and
- * animations on the overview state that depend on the recents implementation.
- */
-public final class PortraitOverviewStateTouchHelper {
-
-    public PortraitOverviewStateTouchHelper(Launcher launcher) {}
-
-    /**
-     * Whether or not {@link PortraitStatesTouchController} should intercept the touch when on the
-     * overview state.
-     *
-     * @param ev the motion event
-     * @return true if we should intercept the motion event
-     */
-    boolean canInterceptTouch(MotionEvent ev) {
-        // Go does not support swiping to all-apps from recents.
-        return false;
-    }
-
-    /**
-     * Whether or not swiping down to leave overview state should return to the currently running
-     * task app.
-     *
-     * @return true if going back should take the user to the currently running task
-     */
-    boolean shouldSwipeDownReturnToApp() {
-        // Go does not support swiping tasks down to launch tasks from recents.
-        return false;
-    }
-
-    /**
-     * Create the animation for going from overview to the task app via swiping.
-     *
-     * @param duration how long the animation should be
-     * @return the animation
-     */
-    PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
-        // Go does not support swiping tasks down to launch tasks from recents.
-        return null;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
deleted file mode 100644
index 92900f2..0000000
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.quickstep.views.IconRecentsView.REMOTE_APP_TO_OVERVIEW_DURATION;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.LauncherAnimationRunner;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Provider for the atomic remote window animation from the app to the overview.
- *
- * @param <T> activity that contains the overview
- */
-final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> implements
-        RemoteAnimationProvider {
-    private static final String TAG = "AppToOverviewAnimationProvider";
-
-    private final ActivityControlHelper<T> mHelper;
-    private final int mTargetTaskId;
-    private IconRecentsView mRecentsView;
-    private AppToOverviewAnimationListener mAnimationReadyListener;
-
-    AppToOverviewAnimationProvider(ActivityControlHelper<T> helper, int targetTaskId) {
-        mHelper = helper;
-        mTargetTaskId = targetTaskId;
-    }
-
-    /**
-     * Set listener to various points in the animation preparing to animate.
-     *
-     * @param listener listener
-     */
-    void setAnimationListener(AppToOverviewAnimationListener listener) {
-        mAnimationReadyListener = listener;
-    }
-
-    /**
-     * Callback for when the activity is ready/initialized.
-     *
-     * @param activity the activity that is ready
-     * @param wasVisible true if it was visible before
-     */
-    boolean onActivityReady(T activity, Boolean wasVisible) {
-        if (mAnimationReadyListener != null) {
-            mAnimationReadyListener.onActivityReady(activity);
-        }
-        ActivityControlHelper.AnimationFactory factory =
-                mHelper.prepareRecentsUI(activity, wasVisible,
-                        false /* animate activity */, (controller) -> {
-                            controller.dispatchOnStart();
-                            ValueAnimator anim = controller.getAnimationPlayer()
-                                    .setDuration(getRecentsLaunchDuration());
-                            anim.setInterpolator(FAST_OUT_SLOW_IN);
-                            anim.start();
-                        });
-        factory.onRemoteAnimationReceived(null);
-        factory.createActivityController(getRecentsLaunchDuration());
-        mRecentsView = activity.getOverviewPanel();
-        return false;
-    }
-
-    /**
-     * Create remote window animation from the currently running app to the overview panel. Should
-     * be called after {@link #onActivityReady}.
-     *
-     * @param targetCompats the target apps
-     * @return animation from app to overview
-     */
-    @Override
-    public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
-        if (mAnimationReadyListener != null) {
-            mAnimationReadyListener.onWindowAnimationCreated();
-        }
-        AnimatorSet anim = new AnimatorSet();
-        if (mRecentsView == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No recents view. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-
-        RemoteAnimationTargetSet targetSet =
-                new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING);
-        mRecentsView.setTransitionedFromApp(!targetSet.isAnimatingHome());
-
-        RemoteAnimationTargetCompat recentsTarget = null;
-        RemoteAnimationTargetCompat closingAppTarget = null;
-
-        for (RemoteAnimationTargetCompat target : targetCompats) {
-            if (target.mode == MODE_OPENING) {
-                recentsTarget = target;
-            } else if (target.mode == MODE_CLOSING && target.taskId == mTargetTaskId) {
-                closingAppTarget = target;
-            }
-        }
-
-        if (closingAppTarget == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No closing app target. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-        if (recentsTarget == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No recents target. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-
-        if (closingAppTarget.activityType == ACTIVITY_TYPE_HOME) {
-            mRecentsView.playRemoteHomeToRecentsAnimation(anim, closingAppTarget, recentsTarget);
-        } else {
-            mRecentsView.playRemoteAppToRecentsAnimation(anim, closingAppTarget, recentsTarget);
-        }
-
-        return anim;
-    }
-
-    @Override
-    public ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
-        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
-                false /* startAtFrontOfQueue */) {
-
-            @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
-                    AnimationResult result) {
-                IconRecentsView recentsView = mRecentsView;
-                if (!recentsView.isReadyForRemoteAnim()) {
-                    recentsView.setOnReadyForRemoteAnimCallback(() -> postAsyncCallback(handler,
-                            () -> onCreateAnimation(targetCompats, result))
-                    );
-                    return;
-                }
-                result.setAnimation(createWindowAnimation(targetCompats), context);
-            }
-        };
-        return ActivityOptionsCompat.makeRemoteAnimation(
-                new RemoteAnimationAdapterCompat(runner, duration,
-                        0 /* statusBarTransitionDelay */));
-    }
-
-    /**
-     * Get duration of animation from app to overview.
-     *
-     * @return duration of animation
-     */
-    long getRecentsLaunchDuration() {
-        return REMOTE_APP_TO_OVERVIEW_DURATION;
-    }
-
-    /**
-     * Listener for various points in the app to overview animation preparing to animate.
-     */
-    interface AppToOverviewAnimationListener {
-        /**
-         * Logic for when activity we're animating to is ready
-         *
-         * @param activity activity to animate to
-         */
-        void onActivityReady(BaseDraggingActivity activity);
-
-        /**
-         * Logic for when we've created the app to recents animation.
-         */
-        void onWindowAnimationCreated();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
deleted file mode 100644
index ce87171..0000000
--- a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-/**
- * Holder for clear all button view in task recycler view.
- */
-final class ClearAllHolder extends ViewHolder {
-    public ClearAllHolder(@NonNull View itemView) {
-        super(itemView);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
deleted file mode 100644
index 808cd72..0000000
--- a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.view.View.ALPHA;
-
-import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
-import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import androidx.recyclerview.widget.SimpleItemAnimator;
-
-import com.android.quickstep.views.TaskItemView;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * An item animator that is only set and used for the transition from the empty loading UI to
- * the filled task content UI. The animation starts from the bottom to top, changing all valid
- * empty item views to be filled and removing all extra empty views.
- */
-public final class ContentFillItemAnimator extends SimpleItemAnimator {
-
-    private static final class PendingAnimation {
-        ViewHolder viewHolder;
-        int animType;
-
-        PendingAnimation(ViewHolder vh, int type) {
-            viewHolder = vh;
-            animType = type;
-        }
-    }
-
-    private static final int ANIM_TYPE_REMOVE = 0;
-    private static final int ANIM_TYPE_CHANGE = 1;
-
-    private static final int ITEM_BETWEEN_DELAY = 40;
-    private static final int ITEM_CHANGE_DURATION = 150;
-    private static final int ITEM_REMOVE_DURATION = 150;
-
-    /**
-     * Animations that have been registered to occur together at the next call of
-     * {@link #runPendingAnimations()} but have not started.
-     */
-    private final ArrayList<PendingAnimation> mPendingAnims = new ArrayList<>();
-
-    /**
-     * Animations that have started and are running.
-     */
-    private final ArrayList<ObjectAnimator> mRunningAnims = new ArrayList<>();
-
-    private Runnable mOnFinishRunnable;
-
-    /**
-     * Set runnable to run after the content fill animation is fully completed.
-     *
-     * @param runnable runnable to run on end
-     */
-    public void setOnAnimationFinishedRunnable(Runnable runnable) {
-        mOnFinishRunnable = runnable;
-    }
-
-    @Override
-    public void setChangeDuration(long changeDuration) {
-        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
-                + "duration changed.");
-    }
-
-    @Override
-    public void setRemoveDuration(long removeDuration) {
-        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
-                + "duration changed.");
-    }
-
-    @Override
-    public boolean animateRemove(ViewHolder holder) {
-        PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
-        mPendingAnims.add(pendAnim);
-        return true;
-    }
-
-    private void animateRemoveImpl(ViewHolder holder, long startDelay) {
-        final View view = holder.itemView;
-        if (holder.itemView.getAlpha() == 0) {
-            // View is already visually removed. We can just get rid of it now.
-            view.setAlpha(1.0f);
-            dispatchRemoveFinished(holder);
-            dispatchFinishedWhenDone();
-            return;
-        }
-        final ObjectAnimator anim = ObjectAnimator.ofFloat(
-                holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
-        anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
-        anim.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        dispatchRemoveStarting(holder);
-                    }
-
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        view.setAlpha(1);
-                        dispatchRemoveFinished(holder);
-                        mRunningAnims.remove(anim);
-                        dispatchFinishedWhenDone();
-                    }
-                }
-        );
-        anim.start();
-        mRunningAnims.add(anim);
-    }
-
-    @Override
-    public boolean animateAdd(ViewHolder holder) {
-        dispatchAddFinished(holder);
-        return false;
-    }
-
-    @Override
-    public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
-            int toY) {
-        dispatchMoveFinished(holder);
-        return false;
-    }
-
-    @Override
-    public boolean animateChange(ViewHolder oldHolder,
-            ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
-        // Only support changes where the holders are the same
-        if (oldHolder == newHolder) {
-            PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
-            mPendingAnims.add(pendAnim);
-            return true;
-        }
-        dispatchChangeFinished(oldHolder, true /* oldItem */);
-        dispatchChangeFinished(newHolder, false /* oldItem */);
-        return false;
-    }
-
-    private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
-        TaskItemView itemView = (TaskItemView) viewHolder.itemView;
-        if (itemView.getAlpha() == 0) {
-            // View is still not visible, so we can finish the change immediately.
-            CONTENT_TRANSITION_PROGRESS.set(itemView, 1.0f);
-            dispatchChangeFinished(viewHolder, true /* oldItem */);
-            dispatchFinishedWhenDone();
-            return;
-        }
-        final ObjectAnimator anim =
-                ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
-        anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
-        anim.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        dispatchChangeStarting(viewHolder, true /* oldItem */);
-                    }
-
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        CONTENT_TRANSITION_PROGRESS.set(itemView, 1.0f);
-                        dispatchChangeFinished(viewHolder, true /* oldItem */);
-                        mRunningAnims.remove(anim);
-                        dispatchFinishedWhenDone();
-                    }
-                }
-        );
-        anim.start();
-        mRunningAnims.add(anim);
-    }
-
-    @Override
-    public void runPendingAnimations() {
-        // Run animations bottom to top.
-        mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
-        int delay = 0;
-        while (!mPendingAnims.isEmpty()) {
-            PendingAnimation curAnim = mPendingAnims.remove(0);
-            ViewHolder vh = curAnim.viewHolder;
-            switch (curAnim.animType) {
-                case ANIM_TYPE_REMOVE:
-                    animateRemoveImpl(vh, delay);
-                    break;
-                case ANIM_TYPE_CHANGE:
-                    animateChangeImpl(vh, delay);
-                    break;
-                default:
-                    break;
-            }
-            delay += ITEM_BETWEEN_DELAY;
-        }
-    }
-
-    @Override
-    public void endAnimation(@NonNull ViewHolder item) {
-        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
-            endPendingAnimation(mPendingAnims.get(i));
-            mPendingAnims.remove(i);
-        }
-        dispatchFinishedWhenDone();
-    }
-
-    @Override
-    public void endAnimations() {
-        if (!isRunning()) {
-            return;
-        }
-        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
-            endPendingAnimation(mPendingAnims.get(i));
-            mPendingAnims.remove(i);
-        }
-        for (int i = mRunningAnims.size() - 1; i >= 0; i--) {
-            ObjectAnimator anim = mRunningAnims.get(i);
-            // This calls the on end animation callback which will set values to their end target.
-            anim.cancel();
-        }
-        dispatchFinishedWhenDone();
-    }
-
-    private void endPendingAnimation(PendingAnimation pendAnim) {
-        ViewHolder item = pendAnim.viewHolder;
-        switch (pendAnim.animType) {
-            case ANIM_TYPE_REMOVE:
-                item.itemView.setAlpha(1.0f);
-                dispatchRemoveFinished(item);
-                break;
-            case ANIM_TYPE_CHANGE:
-                CONTENT_TRANSITION_PROGRESS.set(item.itemView, 1.0f);
-                dispatchChangeFinished(item, true /* oldItem */);
-                break;
-            default:
-                break;
-        }
-    }
-
-    @Override
-    public boolean isRunning() {
-        return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
-    }
-
-    @Override
-    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
-            @NonNull List<Object> payloads) {
-        if (!payloads.isEmpty()
-                && (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
-            return true;
-        }
-        return super.canReuseUpdatedViewHolder(viewHolder, payloads);
-    }
-
-    private void dispatchFinishedWhenDone() {
-        if (!isRunning()) {
-            dispatchAnimationsFinished();
-            if (mOnFinishRunnable != null) {
-                mOnFinishRunnable.run();
-            }
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java b/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java
deleted file mode 100644
index 057b48b..0000000
--- a/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.graphics.Rect;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-
-/**
- * {@link ActivityControlHelper} for recents when the default launcher is different than the
- * currently running one and apps should interact with the {@link RecentsActivity} as opposed
- * to the in-launcher one.
- */
-public final class FallbackActivityControllerHelper extends
-        GoActivityControlHelper<RecentsActivity> {
-
-    public FallbackActivityControllerHelper() { }
-
-    @Override
-    public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        if (activityVisible) {
-            return (transitionLength) -> { };
-        }
-
-        IconRecentsView rv = activity.getOverviewPanel();
-        rv.setUsingRemoteAnimation(true);
-        rv.setAlpha(0);
-
-        return new AnimationFactory() {
-
-            boolean isAnimatingToRecents = false;
-
-            @Override
-            public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) {
-                isAnimatingToRecents = targets != null && targets.isAnimatingHome();
-                if (!isAnimatingToRecents) {
-                    rv.setAlpha(1);
-                }
-                createActivityController(getSwipeUpDestinationAndLength(
-                        activity.getDeviceProfile(), activity, new Rect()));
-            }
-
-            @Override
-            public void createActivityController(long transitionLength) {
-                if (!isAnimatingToRecents) {
-                    return;
-                }
-
-                ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1);
-                anim.setDuration(transitionLength).setInterpolator(LINEAR);
-                AnimatorSet animatorSet = new AnimatorSet();
-                animatorSet.play(anim);
-                callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength));
-            }
-        };
-    }
-
-    @Override
-    public ActivityInitListener createActivityInitListener(
-            BiPredicate<RecentsActivity, Boolean> onInitListener) {
-        return new RecentsActivityTracker(onInitListener);
-    }
-
-    @Nullable
-    @Override
-    public RecentsActivity getCreatedActivity() {
-        return RecentsActivityTracker.getCurrentActivity();
-    }
-
-    @Nullable
-    @Override
-    public IconRecentsView getVisibleRecentsView() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity != null && activity.hasWindowFocus()) {
-            return activity.getOverviewPanel();
-        }
-        return null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        return false;
-    }
-
-    @Override
-    public int getContainerType() {
-        return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER;
-    }
-
-    @Override
-    public void onLaunchTaskSuccess(RecentsActivity activity) { }
-}
diff --git a/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java b/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java
deleted file mode 100644
index a845f93..0000000
--- a/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-/**
- * {@link RecentsToActivityHelper} for when we are using the fallback recents in
- * {@link BaseRecentsActivity}.
- */
-public final class FallbackRecentsToActivityHelper implements RecentsToActivityHelper {
-
-    BaseRecentsActivity mActivity;
-
-    public FallbackRecentsToActivityHelper(BaseRecentsActivity activity) {
-        mActivity = activity;
-    }
-
-    @Override
-    public void leaveRecents() {
-        mActivity.startHome();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/GoActivityControlHelper.java b/go/quickstep/src/com/android/quickstep/GoActivityControlHelper.java
deleted file mode 100644
index 274a347..0000000
--- a/go/quickstep/src/com/android/quickstep/GoActivityControlHelper.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.android.quickstep;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Base activity control helper for Go that stubs out most of the functionality that is not needed
- * for Go.
- *
- * @param <T> activity that contains the overview
- */
-public abstract class GoActivityControlHelper<T extends BaseDraggingActivity> implements
-        ActivityControlHelper<T> {
-
-    @Override
-    public void onTransitionCancelled(T activity, boolean activityVisible) {
-        // Go transitions to overview are all atomic.
-    }
-
-    @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        // TODO Implement outRect depending on where the task should animate to.
-        // Go does not support swipe up gesture.
-        return 0;
-    }
-
-    @Override
-    public void onSwipeUpToRecentsComplete(T activity) {
-        // Go does not support swipe up gesture.
-    }
-
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        // Go does not support assistant visibility transitions.
-    }
-
-    @Override
-    public HomeAnimationFactory prepareHomeUI(T activity) {
-        // Go does not support gestures from app to home.
-        return null;
-    }
-
-    @Override
-    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
-        // Go does not support gestures to overview.
-        return null;
-    }
-
-    @Override
-    public boolean shouldMinimizeSplitScreen() {
-        // Go does not support split screen.
-        return true;
-    }
-
-    @Override
-    public boolean isInLiveTileMode() {
-        // Go does not support live tiles.
-        return false;
-    }
-
-    @Override
-    public void onLaunchTaskFailed(T activity) {
-        // Go does not support gestures from one task to another.
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java b/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
deleted file mode 100644
index facf0d2..0000000
--- a/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import android.app.Fragment;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-
-public class IconRecentsFragment extends Fragment {
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
-            @Nullable Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.icon_recents_root_view, container, false);
-    }
-}
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java b/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java
deleted file mode 100644
index b0d9cda..0000000
--- a/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherInitListener;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-
-/**
- * {@link ActivityControlHelper} for the in-launcher recents.
- * TODO: Implement the app to overview animation functionality
- */
-public final class LauncherActivityControllerHelper extends GoActivityControlHelper<Launcher> {
-
-    @Override
-    public AnimationFactory prepareRecentsUI(Launcher activity,
-            boolean activityVisible, boolean animateActivity,
-            Consumer<AnimatorPlaybackController> callback) {
-        LauncherState fromState = activity.getStateManager().getState();
-        activity.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(true);
-        //TODO: Implement this based off where the recents view needs to be for app => recents anim.
-        return new AnimationFactory() {
-            @Override
-            public void createActivityController(long transitionLength) {
-                callback.accept(activity.getStateManager().createAnimationToNewWorkspace(
-                        fromState, OVERVIEW, transitionLength));
-            }
-
-            @Override
-            public void onTransitionCancelled() {}
-        };
-    }
-
-    @Override
-    public ActivityInitListener createActivityInitListener(
-            BiPredicate<Launcher, Boolean> onInitListener) {
-        return new LauncherInitListener(onInitListener);
-    }
-
-    @Override
-    public Launcher getCreatedActivity() {
-        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-        if (app == null) {
-            return null;
-        }
-        return (Launcher) app.getModel().getCallback();
-    }
-
-    private Launcher getVisibleLauncher() {
-        Launcher launcher = getCreatedActivity();
-        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
-                launcher : null;
-    }
-
-    @Override
-    public IconRecentsView getVisibleRecentsView() {
-        Launcher launcher = getVisibleLauncher();
-        return launcher != null && launcher.getStateManager().getState().overviewUi
-                ? launcher.getOverviewPanel() : null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        Launcher launcher = getVisibleLauncher();
-        if (launcher == null) {
-            return false;
-        }
-        launcher.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(false);
-        launcher.getUserEventDispatcher().logActionCommand(
-                LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                getContainerType(),
-                LauncherLogProto.ContainerType.TASKSWITCHER);
-        launcher.getStateManager().goToState(OVERVIEW,
-                launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
-        return true;
-    }
-
-    @Override
-    public int getContainerType() {
-        final Launcher launcher = getVisibleLauncher();
-        return launcher != null ? launcher.getStateManager().getState().containerType
-                : LauncherLogProto.ContainerType.APP;
-    }
-
-    @Override
-    public void onLaunchTaskSuccess(Launcher launcher) {
-        launcher.getStateManager().moveToRestState();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
deleted file mode 100644
index 216972c..0000000
--- a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
-import com.android.quickstep.AppToOverviewAnimationProvider.AppToOverviewAnimationListener;
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.LatencyTrackerCompat;
-
-/**
- * Helper class to handle various atomic commands for switching between Overview.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class OverviewCommandHelper {
-
-    private final Context mContext;
-    private final ActivityManagerWrapper mAM;
-    private final RecentsModel mRecentsModel;
-    private final OverviewComponentObserver mOverviewComponentObserver;
-
-    private long mLastToggleTime;
-
-    public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
-        mContext = context;
-        mAM = ActivityManagerWrapper.getInstance();
-        mRecentsModel = RecentsModel.INSTANCE.get(mContext);
-        mOverviewComponentObserver = observer;
-    }
-
-    public void onOverviewToggle() {
-        // If currently screen pinning, do not enter overview
-        if (mAM.isScreenPinningActive()) {
-            return;
-        }
-
-        mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
-    }
-
-    public void onOverviewShown(boolean triggeredFromAltTab) {
-        MAIN_EXECUTOR.execute(new ShowRecentsCommand());
-    }
-
-    public void onOverviewHidden() {
-        MAIN_EXECUTOR.execute(new HideRecentsCommand());
-    }
-
-    public void onTip(int actionType, int viewType) {
-        MAIN_EXECUTOR.execute(() ->
-                UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
-    }
-
-    private class ShowRecentsCommand extends RecentsActivityCommand {
-
-        @Override
-        protected boolean handleCommand(long elapsedTime) {
-            return mHelper.getVisibleRecentsView() != null;
-        }
-    }
-
-    private class HideRecentsCommand extends RecentsActivityCommand {
-
-        @Override
-        protected boolean handleCommand(long elapsedTime) {
-            IconRecentsView recents = (IconRecentsView) mHelper.getVisibleRecentsView();
-            if (recents == null) {
-                return false;
-            }
-            recents.handleOverviewCommand();
-            return true;
-        }
-    }
-
-    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
-
-        protected final ActivityControlHelper<T> mHelper;
-        private final long mCreateTime;
-
-        private final long mToggleClickedTime = SystemClock.uptimeMillis();
-        private boolean mUserEventLogged;
-        private ActivityInitListener mListener;
-
-        public RecentsActivityCommand() {
-            mHelper = mOverviewComponentObserver.getActivityControlHelper();
-            mCreateTime = SystemClock.elapsedRealtime();
-
-            // Preload the plan
-            mRecentsModel.getTasks(null);
-        }
-
-        @Override
-        public void run() {
-            long elapsedTime = mCreateTime - mLastToggleTime;
-            mLastToggleTime = mCreateTime;
-
-            if (handleCommand(elapsedTime)) {
-                // Command already handled.
-                return;
-            }
-
-            if (mHelper.switchToRecentsIfVisible(null /* onCompleteCallback */)) {
-                // If successfully switched, then return
-                return;
-            }
-
-            AppToOverviewAnimationProvider<T> provider =
-                    new AppToOverviewAnimationProvider<>(mHelper, RecentsModel.getRunningTaskId());
-            provider.setAnimationListener(
-                    new AppToOverviewAnimationListener() {
-                        @Override
-                        public void onActivityReady(BaseDraggingActivity activity) {
-                            if (!mUserEventLogged) {
-                                activity.getUserEventDispatcher().logActionCommand(
-                                        LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                                        mHelper.getContainerType(),
-                                        LauncherLogProto.ContainerType.TASKSWITCHER);
-                                mUserEventLogged = true;
-                            }
-                        }
-
-                        @Override
-                        public void onWindowAnimationCreated() {
-                            if (LatencyTrackerCompat.isEnabled(mContext)) {
-                                LatencyTrackerCompat.logToggleRecents(
-                                        (int) (SystemClock.uptimeMillis() - mToggleClickedTime));
-                            }
-
-                            mListener.unregister();
-                        }
-                    });
-
-            // Otherwise, start overview.
-            mListener = mHelper.createActivityInitListener(provider::onActivityReady);
-            mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
-                    provider, mContext, MAIN_EXECUTOR.getHandler(),
-                    provider.getRecentsLaunchDuration());
-        }
-
-        protected boolean handleCommand(long elapsedTime) {
-            IconRecentsView recents = mHelper.getVisibleRecentsView();
-            if (recents != null) {
-                recents.handleOverviewCommand();
-                return true;
-            } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
-                // The user tried to launch back into overview too quickly, either after
-                // launching an app, or before overview has actually shown, just ignore for now
-                return true;
-            }
-            return false;
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/RecentsActivity.java b/go/quickstep/src/com/android/quickstep/RecentsActivity.java
deleted file mode 100644
index 7f813ce..0000000
--- a/go/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.app.ActivityOptions;
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.fallback.GoRecentsActivityRootView;
-import com.android.quickstep.views.IconRecentsView;
-
-/**
- * A recents activity that displays recent tasks with an icon and small snapshot.
- */
-public final class RecentsActivity extends BaseRecentsActivity {
-
-    private GoRecentsActivityRootView mRecentsRootView;
-    private IconRecentsView mIconRecentsView;
-
-    @Override
-    protected void initViews() {
-        setContentView(R.layout.fallback_recents_activity);
-        mRecentsRootView = findViewById(R.id.drag_layer);
-        mIconRecentsView = findViewById(R.id.overview_panel);
-        mIconRecentsView.setRecentsToActivityHelper(new FallbackRecentsToActivityHelper(this));
-        mIconRecentsView.setShowStatusBarForegroundScrim(true);
-    }
-
-    @Override
-    protected void reapplyUi() {
-        // No-op. Insets are automatically re-applied in the root view.
-    }
-
-    @Override
-    public BaseDragLayer getDragLayer() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public View getRootView() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public <T extends View> T getOverviewPanel() {
-        return (T) mIconRecentsView;
-    }
-
-    @Override
-    public ActivityOptions getActivityLaunchOptions(View v) {
-        // Stubbed. Recents launch animation will come from the recents view itself.
-        return null;
-    }
-
-    @Override
-    protected void onResume() {
-        mIconRecentsView.onBeginTransitionToOverview();
-        super.onResume();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java b/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java
deleted file mode 100644
index 8f3b707..0000000
--- a/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-/**
- * Generic interface providing methods to the recents implementation that allow it to callback to
- * the containing activity.
- */
-public interface RecentsToActivityHelper {
-
-    /**
-     * The default action to take when leaving/closing recents. In general, this should be used to
-     * go to the appropriate home state.
-     */
-    void leaveRecents();
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
deleted file mode 100644
index f49fa3e..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
-import static com.android.quickstep.TaskUtils.getLaunchComponentKeyForTask;
-
-import android.app.ActivityOptions;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-/**
- * Controller that provides logic for task-related commands on recents and updating the model/view
- * as appropriate.
- */
-public final class TaskActionController {
-
-    private final TaskListLoader mLoader;
-    private final TaskAdapter mAdapter;
-    private final StatsLogManager mStatsLogManager;
-
-    public TaskActionController(TaskListLoader loader, TaskAdapter adapter,
-            StatsLogManager logManager) {
-        mLoader = loader;
-        mAdapter = adapter;
-        mStatsLogManager = logManager;
-    }
-
-    /**
-     * Launch the task associated with the task holder, animating into the app from the task view.
-     *
-     * @param viewHolder the task view holder to launch
-     */
-    public void launchTaskFromView(@NonNull TaskHolder viewHolder) {
-        if (!viewHolder.getTask().isPresent()) {
-            return;
-        }
-        TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
-        View v = itemView.getThumbnailView();
-        int left = 0;
-        int top = 0;
-        int width = v.getMeasuredWidth();
-        int height = v.getMeasuredHeight();
-
-        TaskKey key = viewHolder.getTask().get().key;
-        ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(key, opts,
-                null /* resultCallback */, null /* resultCallbackHandler */);
-        mStatsLogManager.logTaskLaunch(null /* view */, getLaunchComponentKeyForTask(key));
-    }
-
-    /**
-     * Launch the task directly with a basic animation.
-     *
-     * @param task the task to launch
-     */
-    public void launchTask(@NonNull Task task) {
-        ActivityOptions opts = ActivityOptions.makeBasic();
-        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(task.key, opts,
-                null /* resultCallback */, null /* resultCallbackHandler */);
-        mStatsLogManager.logTaskLaunch(null /* view */, getLaunchComponentKeyForTask(task.key));
-    }
-
-    /**
-     * Removes the task holder and the task, updating the model and the view.
-     *
-     * @param viewHolder the task view holder to remove
-     */
-    public void removeTask(TaskHolder viewHolder) {
-        if (!viewHolder.getTask().isPresent()) {
-            return;
-        }
-        int position = viewHolder.getAdapterPosition();
-        Task task = viewHolder.getTask().get();
-        ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-        mLoader.removeTask(task);
-        mAdapter.notifyItemRemoved(position);
-        // TODO(b/131840601): Add logging point for removal.
-    }
-
-    /**
-     * Clears all tasks and updates the model and view.
-     */
-    public void clearAllTasks() {
-        int count = mAdapter.getItemCount();
-        ActivityManagerWrapper.getInstance().removeAllRecentTasks();
-        mLoader.clearAllTasks();
-        mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
deleted file mode 100644
index 509bf29..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.Button;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import com.android.launcher3.R;
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
- * appropriate {@link Task} from the recents task list.
- */
-public final class TaskAdapter extends Adapter<ViewHolder> {
-
-    public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
-    public static final int MAX_TASKS_TO_DISPLAY = 6;
-    public static final int TASKS_START_POSITION = 1;
-
-    public static final int ITEM_TYPE_TASK = 0;
-    public static final int ITEM_TYPE_CLEAR_ALL = 1;
-
-    private static final String TAG = "TaskAdapter";
-    private final TaskListLoader mLoader;
-    private TaskActionController mTaskActionController;
-    private OnClickListener mClearAllListener;
-    private boolean mIsShowingLoadingUi;
-
-    public TaskAdapter(@NonNull TaskListLoader loader) {
-        mLoader = loader;
-    }
-
-    public void setActionController(TaskActionController taskActionController) {
-        mTaskActionController = taskActionController;
-    }
-
-    public void setOnClearAllClickListener(OnClickListener listener) {
-        mClearAllListener = listener;
-    }
-
-    /**
-     * Sets all positions in the task adapter to loading views, binding new views if necessary.
-     * This changes the task adapter's view of the data, so the appropriate notify events should be
-     * called in addition to this method to reflect the changes.
-     *
-     * @param isShowingLoadingUi true to bind loading task views to all positions, false to return
-     *                           to the real data
-     */
-    public void setIsShowingLoadingUi(boolean isShowingLoadingUi) {
-        mIsShowingLoadingUi = isShowingLoadingUi;
-    }
-
-    @Override
-    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        switch (viewType) {
-            case ITEM_TYPE_TASK:
-                TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.task_item_view, parent, false);
-                TaskHolder taskHolder = new TaskHolder(itemView);
-                itemView.setOnClickListener(
-                        view -> mTaskActionController.launchTaskFromView(taskHolder));
-                return taskHolder;
-            case ITEM_TYPE_CLEAR_ALL:
-                View clearView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.clear_all_button, parent, false);
-                ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
-                Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
-                clearViewButton.setOnClickListener(mClearAllListener);
-                return clearAllHolder;
-            default:
-                throw new IllegalArgumentException("No known holder for item type: " + viewType);
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(ViewHolder holder, int position) {
-        onBindViewHolderInternal(holder, position, false /* willAnimate */);
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull ViewHolder holder, int position,
-            @NonNull List<Object> payloads) {
-        if (payloads.isEmpty()) {
-            super.onBindViewHolder(holder, position, payloads);
-            return;
-        }
-        int changeType = (int) payloads.get(0);
-        if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
-            // Bind in preparation for animation
-            onBindViewHolderInternal(holder, position, true /* willAnimate */);
-        } else {
-            throw new IllegalArgumentException("Payload content is not a valid change event type: "
-                    + changeType);
-        }
-    }
-
-    private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
-            boolean willAnimate) {
-        int itemType = getItemViewType(position);
-        switch (itemType) {
-            case ITEM_TYPE_TASK:
-                TaskHolder taskHolder = (TaskHolder) holder;
-                if (mIsShowingLoadingUi) {
-                    taskHolder.bindEmptyUi();
-                    return;
-                }
-                List<Task> tasks = mLoader.getCurrentTaskList();
-                int taskPos = position - TASKS_START_POSITION;
-                if (taskPos >= tasks.size()) {
-                    // Task list has updated.
-                    return;
-                }
-                Task task = tasks.get(taskPos);
-                taskHolder.bindTask(task, willAnimate /* willAnimate */);
-                mLoader.loadTaskIconAndLabel(task, () -> {
-                    // Ensure holder still has the same task.
-                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
-                        taskHolder.getTaskItemView().setIcon(task.icon);
-                        taskHolder.getTaskItemView().setLabel(task.titleDescription);
-                    }
-                });
-                mLoader.loadTaskThumbnail(task, () -> {
-                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
-                        taskHolder.getTaskItemView().setThumbnail(task.thumbnail);
-                    }
-                });
-                break;
-            case ITEM_TYPE_CLEAR_ALL:
-                // Nothing to bind.
-                break;
-            default:
-                throw new IllegalArgumentException("No known holder for item type: " + itemType);
-        }
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        // Bottom is always clear all button.
-        return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
-    }
-
-    @Override
-    public int getItemCount() {
-        int itemCount = TASKS_START_POSITION;
-        if (mIsShowingLoadingUi) {
-            // Show loading version of all items.
-            itemCount += MAX_TASKS_TO_DISPLAY;
-        } else {
-            itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
-        }
-        return itemCount;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
deleted file mode 100644
index 49b6aaa..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.Optional;
-
-/**
- * A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
- * etc.) to the view.
- */
-public final class TaskHolder extends ViewHolder {
-
-    private final TaskItemView mTaskItemView;
-    private Task mTask;
-
-    public TaskHolder(TaskItemView itemView) {
-        super(itemView);
-        mTaskItemView = itemView;
-    }
-
-    public TaskItemView getTaskItemView() {
-        return mTaskItemView;
-    }
-
-    /**
-     * Bind the task model to the holder. This will take the current task content in the task
-     * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
-     * the content to animate to at a later time. If the task does not have all its content loaded,
-     * the view will prepare appropriate default placeholders and it is the callers responsibility
-     * to change them at a later time.
-     *
-     * Regardless of whether it is animating, input handlers will be bound immediately (see
-     * {@link TaskActionController}).
-     *
-     * @param task the task to bind to the view
-     * @param willAnimate true if UI should animate in later, false if it should apply immediately
-     */
-    public void bindTask(@NonNull Task task, boolean willAnimate) {
-        mTask = task;
-        if (willAnimate) {
-            mTaskItemView.startContentAnimation(task.icon, task.thumbnail, task.titleDescription);
-        } else {
-            mTaskItemView.setIcon(task.icon);
-            mTaskItemView.setThumbnail(task.thumbnail);
-            mTaskItemView.setLabel(task.titleDescription);
-        }
-    }
-
-    /**
-     * Bind a generic empty UI to the holder to make it clear that the item is loading/unbound and
-     * should not be expected to react to user input.
-     */
-    public void bindEmptyUi() {
-        mTask = null;
-        mTaskItemView.resetToEmptyUi();
-    }
-
-    /**
-     * Gets the task currently bound to this view. May be null if task holder is in a loading state.
-     *
-     * @return the current task
-     */
-    public Optional<Task> getTask() {
-        return Optional.ofNullable(mTask);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
deleted file mode 100644
index 1335cac..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * This class is responsible for maintaining the list of tasks and the task content. The list must
- * be updated explicitly with {@link #loadTaskList} whenever the list needs to be
- * up-to-date.
- */
-public final class TaskListLoader {
-
-    private final RecentsModel mRecentsModel;
-
-    private ArrayList<Task> mTaskList = new ArrayList<>();
-    private int mTaskListChangeId;
-
-    public TaskListLoader(Context context) {
-        mRecentsModel = RecentsModel.INSTANCE.get(context);
-    }
-
-    /**
-     * Returns the current task list as of the last completed load (see {@link #loadTaskList}) as a
-     * read-only list. This list of tasks is not guaranteed to have all content loaded.
-     *
-     * @return the current list of tasks
-     */
-    public List<Task> getCurrentTaskList() {
-        return Collections.unmodifiableList(mTaskList);
-    }
-
-    /**
-     * Whether or not the loader needs to load data to be up to date. This can return true if the
-     * task list is already up to date OR there is already a load in progress for the task list to
-     * become up to date.
-     *
-     * @return true if already up to date or load in progress, false otherwise
-     */
-    public boolean needsToLoad() {
-        return !mRecentsModel.isTaskListValid(mTaskListChangeId);
-    }
-
-    /**
-     * Fetches the most recent tasks and updates the task list asynchronously. This call does not
-     * provide guarantees the task content (icon, thumbnail, label) are loaded but will fill in
-     * what it has. May run the callback immediately if there have been no changes in the task
-     * list since the start of the last load.
-     *
-     * @param onLoadedCallback callback to run when task list is loaded
-     */
-    public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onLoadedCallback) {
-        if (!needsToLoad()) {
-            if (onLoadedCallback != null) {
-                onLoadedCallback.accept(mTaskList);
-            }
-            return;
-        }
-        // TODO: Look into error checking / more robust handling for when things go wrong.
-        mTaskListChangeId = mRecentsModel.getTasks(loadedTasks -> {
-            ArrayList<Task> tasks = new ArrayList<>(loadedTasks);
-            // Reverse tasks to put most recent at the bottom of the view
-            Collections.reverse(tasks);
-            // Load task content
-            for (Task task : tasks) {
-                int loadedPos = mTaskList.indexOf(task);
-                if (loadedPos == -1) {
-                    continue;
-                }
-                Task loadedTask = mTaskList.get(loadedPos);
-                task.icon = loadedTask.icon;
-                task.titleDescription = loadedTask.titleDescription;
-                task.thumbnail = loadedTask.thumbnail;
-            }
-            mTaskList = tasks;
-            onLoadedCallback.accept(tasks);
-        });
-    }
-
-    /**
-     * Load task icon and label asynchronously if it is not already loaded in the task. If the task
-     * already has an icon, this calls the callback immediately.
-     *
-     * @param task task to update with icon + label
-     * @param onLoadedCallback callback to run when task has icon and label
-     */
-    public void loadTaskIconAndLabel(Task task, @Nullable Runnable onLoadedCallback) {
-        mRecentsModel.getIconCache().updateIconInBackground(task,
-                loadedTask -> onLoadedCallback.run());
-    }
-
-    /**
-     * Load thumbnail asynchronously if not already loaded in the task. If the task already has a
-     * thumbnail or if the thumbnail is cached, this calls the callback immediately.
-     *
-     * @param task task to update with the thumbnail
-     * @param onLoadedCallback callback to run when task has thumbnail
-     */
-    public void loadTaskThumbnail(Task task, @Nullable Runnable onLoadedCallback) {
-        mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
-                thumbnail -> onLoadedCallback.run());
-    }
-
-    /**
-     * Removes the task from the current task list.
-     */
-    void removeTask(Task task) {
-        mTaskList.remove(task);
-    }
-
-    /**
-     * Clears the current task list.
-     */
-    void clearAllTasks() {
-        mTaskList.clear();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
deleted file mode 100644
index 57f49d6..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
-
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
-
-import android.graphics.Canvas;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import java.util.function.Consumer;
-
-/**
- * Callback for swipe input on {@link TaskHolder} views in the recents view.
- */
-public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
-
-    private final Consumer<TaskHolder> mOnTaskSwipeCallback;
-
-    public TaskSwipeCallback(Consumer<TaskHolder> onTaskSwipeCallback) {
-        super(0 /* dragDirs */, RIGHT);
-        mOnTaskSwipeCallback = onTaskSwipeCallback;
-    }
-
-    @Override
-    public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder,
-            ViewHolder target) {
-        return false;
-    }
-
-    @Override
-    public void onSwiped(ViewHolder viewHolder, int direction) {
-        if (direction == RIGHT) {
-            mOnTaskSwipeCallback.accept((TaskHolder) viewHolder);
-        }
-    }
-
-    @Override
-    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
-            @NonNull ViewHolder viewHolder, float dX, float dY, int actionState,
-            boolean isCurrentlyActive) {
-        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
-            float alpha = 1.0f - dX / (float) viewHolder.itemView.getWidth();
-            viewHolder.itemView.setAlpha(alpha);
-        }
-        super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
-                    actionState, isCurrentlyActive);
-    }
-
-    @Override
-    public int getSwipeDirs(@NonNull RecyclerView recyclerView,
-            @NonNull ViewHolder viewHolder) {
-        if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
-            // Clear all button should not be swipable.
-            return 0;
-        }
-        return super.getSwipeDirs(recyclerView, viewHolder);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
deleted file mode 100644
index 922e68a..0000000
--- a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.graphics.Shader.TileMode.CLAMP;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.R;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-
-/**
- * Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
- * orientation the thumbnail was taken in matches the desired orientation. In addition, the
- * thumbnail always fills into the containing bounds.
- */
-public final class ThumbnailDrawable extends Drawable {
-
-    private final Paint mPaint = new Paint();
-    private final Matrix mMatrix = new Matrix();
-    private final ThumbnailData mThumbnailData;
-    private final BitmapShader mShader;
-    private final RectF mDestRect = new RectF();
-    private final int mCornerRadius;
-    private int mRequestedOrientation;
-
-    public ThumbnailDrawable(Resources res, @NonNull ThumbnailData thumbnailData,
-            int requestedOrientation) {
-        mThumbnailData = thumbnailData;
-        mRequestedOrientation = requestedOrientation;
-        mCornerRadius = (int) res.getDimension(R.dimen.task_thumbnail_corner_radius);
-        mShader = new BitmapShader(mThumbnailData.thumbnail, CLAMP, CLAMP);
-        mPaint.setShader(mShader);
-        mPaint.setAntiAlias(true);
-        updateMatrix();
-    }
-
-    /**
-     * Set the requested orientation.
-     *
-     * @param orientation the orientation we want the thumbnail to be in
-     */
-    public void setRequestedOrientation(int orientation) {
-        if (mRequestedOrientation != orientation) {
-            mRequestedOrientation = orientation;
-            updateMatrix();
-        }
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        if (mThumbnailData.thumbnail == null) {
-            return;
-        }
-        canvas.drawRoundRect(mDestRect, mCornerRadius, mCornerRadius, mPaint);
-    }
-
-    @Override
-    protected void onBoundsChange(Rect bounds) {
-        super.onBoundsChange(bounds);
-        mDestRect.set(bounds);
-        updateMatrix();
-    }
-
-    @Override
-    public void setAlpha(int alpha) {
-        final int oldAlpha = mPaint.getAlpha();
-        if (alpha != oldAlpha) {
-            mPaint.setAlpha(alpha);
-            invalidateSelf();
-        }
-    }
-
-    @Override
-    public int getAlpha() {
-        return mPaint.getAlpha();
-    }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) {
-        mPaint.setColorFilter(colorFilter);
-        invalidateSelf();
-    }
-
-    @Override
-    public ColorFilter getColorFilter() {
-        return mPaint.getColorFilter();
-    }
-
-    @Override
-    public int getOpacity() {
-        return PixelFormat.TRANSLUCENT;
-    }
-
-    private void updateMatrix() {
-        if (mThumbnailData.thumbnail == null) {
-            return;
-        }
-        mMatrix.reset();
-        float scaleX;
-        float scaleY;
-        Rect bounds = getBounds();
-        Bitmap thumbnail = mThumbnailData.thumbnail;
-        if (mRequestedOrientation != mThumbnailData.orientation) {
-            // Rotate and translate so that top left is the same.
-            mMatrix.postRotate(90, 0, 0);
-            mMatrix.postTranslate(thumbnail.getHeight(), 0);
-
-            scaleX = (float) bounds.width() / thumbnail.getHeight();
-            scaleY = (float) bounds.height() / thumbnail.getWidth();
-        } else {
-            scaleX = (float) bounds.width() / thumbnail.getWidth();
-            scaleY = (float) bounds.height() / thumbnail.getHeight();
-        }
-        // Scale to fill.
-        mMatrix.postScale(scaleX, scaleY);
-        mShader.setLocalMatrix(mMatrix);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
deleted file mode 100644
index 19dd82f..0000000
--- a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
-
-import android.annotation.TargetApi;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Region;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.systemui.shared.recents.IOverviewProxy;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-
-/**
- * Service connected by system-UI for handling touch interaction.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class TouchInteractionService extends Service {
-
-    private static final String TAG = "GoTouchInteractionService";
-    private boolean mIsUserUnlocked;
-    private BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
-                initWhenUserUnlocked();
-            }
-        }
-    };
-
-    private final IBinder mMyBinder = new IOverviewProxy.Stub() {
-
-        @Override
-        public void onActiveNavBarRegionChanges(Region region) throws RemoteException { }
-
-        @Override
-        public void onInitialize(Bundle bundle) throws RemoteException {
-            ISystemUiProxy iSystemUiProxy = ISystemUiProxy.Stub
-                    .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            mRecentsModel.setSystemUiProxy(iSystemUiProxy);
-        }
-
-        @Override
-        public void onOverviewToggle() {
-            if (mIsUserUnlocked) {
-                mOverviewCommandHelper.onOverviewToggle();
-            }
-        }
-
-        @Override
-        public void onOverviewShown(boolean triggeredFromAltTab) {
-            if (mIsUserUnlocked) {
-                mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
-            }
-        }
-
-        @Override
-        public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
-            if (mIsUserUnlocked && triggeredFromAltTab && !triggeredFromHomeKey) {
-                // onOverviewShownFromAltTab hides the overview and ends at the target app
-                mOverviewCommandHelper.onOverviewHidden();
-            }
-        }
-
-        @Override
-        public void onTip(int actionType, int viewType) {
-            if (mIsUserUnlocked) {
-                mOverviewCommandHelper.onTip(actionType, viewType);
-            }
-        }
-
-        @Override
-        public void onAssistantAvailable(boolean available) {
-            // TODO handle assistant
-        }
-
-        @Override
-        public void onAssistantVisibilityChanged(float visibility) {
-            // TODO handle assistant
-        }
-
-        public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
-                boolean gestureSwipeLeft) {
-        }
-
-        public void onSystemUiStateChanged(int stateFlags) {
-            // To be implemented
-        }
-
-        /** Deprecated methods **/
-        public void onQuickStep(MotionEvent motionEvent) { }
-
-        public void onQuickScrubEnd() { }
-
-        public void onQuickScrubProgress(float progress) { }
-
-        public void onQuickScrubStart() { }
-
-        public void onPreMotionEvent(int downHitTarget) { }
-
-        public void onMotionEvent(MotionEvent ev) { }
-
-        public void onBind(ISystemUiProxy iSystemUiProxy) {
-            mRecentsModel.setSystemUiProxy(iSystemUiProxy);
-        }
-    };
-
-    private static boolean sConnected = false;;
-
-    public static boolean isConnected() {
-        return sConnected;
-    }
-
-    private RecentsModel mRecentsModel;
-    private OverviewComponentObserver mOverviewComponentObserver;
-    private OverviewCommandHelper mOverviewCommandHelper;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        if (UserManagerCompat.getInstance(this).isUserUnlocked(Process.myUserHandle())) {
-            initWhenUserUnlocked();
-        } else {
-            mIsUserUnlocked = false;
-            registerReceiver(mUserUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
-        }
-
-        sConnected = true;
-    }
-
-    private void initWhenUserUnlocked() {
-        mRecentsModel = RecentsModel.INSTANCE.get(this);
-        mOverviewComponentObserver = new OverviewComponentObserver(this);
-        mOverviewCommandHelper = new OverviewCommandHelper(this,
-                mOverviewComponentObserver);
-        mIsUserUnlocked = true;
-        Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
-    }
-
-    @Override
-    public void onDestroy() {
-        if (mIsUserUnlocked) {
-            mOverviewComponentObserver.onDestroy();
-        }
-        Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
-        sConnected = false;
-        super.onDestroy();
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Touch service connected");
-        }
-        return mMyBinder;
-    }
-
-    public static boolean isInitialized() {
-        return true;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java b/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java
deleted file mode 100644
index b550011..0000000
--- a/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.content.Context;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.WindowInsets;
-
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
-
-/**
- * Minimal implementation of {@link BaseDragLayer} for Go's fallback recents activity.
- */
-public final class GoRecentsActivityRootView extends BaseDragLayer<RecentsActivity> {
-    public GoRecentsActivityRootView(Context context, AttributeSet attrs) {
-        super(context, attrs, 1 /* alphaChannelCount */);
-        // Go leaves touch control to the view itself.
-        mControllers = new TouchController[0];
-        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | SYSTEM_UI_FLAG_LAYOUT_STABLE);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        if (insets.equals(mInsets)) {
-            return;
-        }
-        super.setInsets(insets);
-    }
-
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        Insets sysInsets = insets.getSystemWindowInsets();
-        setInsets(new Rect(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom));
-        return insets.consumeSystemWindowInsets();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java b/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
deleted file mode 100644
index fb89013..0000000
--- a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import com.android.launcher3.Launcher;
-
-/** Empty class, only exists so that l3goWithQuickstepIconRecentsDebug compiles. */
-public class ShelfPeekAnim {
-    public ShelfPeekAnim(Launcher launcher) {
-    }
-
-    public enum ShelfAnimState {
-    }
-
-    public boolean isPeeking() {
-        return false;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
deleted file mode 100644
index 87b4d4e..0000000
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ /dev/null
@@ -1,942 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-
-import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
-
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_TASK;
-import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
-import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
-import static com.android.quickstep.util.RemoteAnimationProvider.getLayer;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.util.ArraySet;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
-import android.view.ViewDebug;
-import android.view.ViewTreeObserver;
-import android.view.animation.PathInterpolator;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
-import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
-import com.android.launcher3.util.Themes;
-import com.android.quickstep.ContentFillItemAnimator;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsToActivityHelper;
-import com.android.quickstep.TaskActionController;
-import com.android.quickstep.TaskAdapter;
-import com.android.quickstep.TaskHolder;
-import com.android.quickstep.TaskListLoader;
-import com.android.quickstep.TaskSwipeCallback;
-import com.android.quickstep.util.MultiValueUpdateListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
- * base.
- */
-public final class IconRecentsView extends FrameLayout implements Insettable {
-
-    public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
-            new FloatProperty<IconRecentsView>("contentAlpha") {
-                @Override
-                public void setValue(IconRecentsView view, float v) {
-                    ALPHA.set(view, v);
-                    if (view.getVisibility() != VISIBLE && v > 0) {
-                        view.setVisibility(VISIBLE);
-                    } else if (view.getVisibility() != GONE && v == 0){
-                        view.setVisibility(GONE);
-                    }
-                }
-
-                @Override
-                public Float get(IconRecentsView view) {
-                    return ALPHA.get(view);
-                }
-            };
-    private static final long CROSSFADE_DURATION = 300;
-    private static final long LAYOUT_ITEM_ANIMATE_IN_DURATION = 150;
-    private static final long LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN = 40;
-    private static final long ITEM_ANIMATE_OUT_DURATION = 150;
-    private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
-    private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
-    private static final long CLEAR_ALL_FADE_DELAY = 120;
-
-    private static final long REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION = 300;
-    private static final long REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION = 400;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY = 200;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_DURATION = 217;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY = 33;
-
-    private static final PathInterpolator FAST_OUT_SLOW_IN_1 =
-            new PathInterpolator(.4f, 0f, 0f, 1f);
-    private static final PathInterpolator FAST_OUT_SLOW_IN_2 =
-            new PathInterpolator(.5f, 0f, 0f, 1f);
-    private static final LinearOutSlowInInterpolator OUT_SLOW_IN =
-            new LinearOutSlowInInterpolator();
-
-    public static final long REMOTE_APP_TO_OVERVIEW_DURATION =
-            REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION;
-
-    /**
-     * A ratio representing the view's relative placement within its padded space. For example, 0
-     * is top aligned and 0.5 is centered vertically.
-     */
-    @ViewDebug.ExportedProperty(category = "launcher")
-
-    private final Context mContext;
-    private final TaskListLoader mTaskLoader;
-    private final TaskAdapter mTaskAdapter;
-    private final LinearLayoutManager mTaskLayoutManager;
-    private final TaskActionController mTaskActionController;
-    private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
-    private final ContentFillItemAnimator mLoadingContentItemAnimator =
-            new ContentFillItemAnimator();
-    private final BaseActivity mActivity;
-    private final Drawable mStatusBarForegroundScrim;
-
-    private RecentsToActivityHelper mActivityHelper;
-    private RecyclerView mTaskRecyclerView;
-    private View mShowingContentView;
-    private View mEmptyView;
-    private View mContentView;
-    private boolean mTransitionedFromApp;
-    private boolean mUsingRemoteAnimation;
-    private boolean mStartedEnterAnimation;
-    private boolean mShowStatusBarForegroundScrim;
-    private AnimatorSet mLayoutAnimation;
-    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
-    private Rect mInsets;
-    private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView taskView = itemViews.get(i);
-            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
-            Optional<Task> optTask = taskHolder.getTask();
-            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
-                Task task = optTask.get();
-                // Update thumbnail on the task.
-                task.thumbnail = thumbnailData;
-                taskView.setThumbnail(thumbnailData);
-                return task;
-            }
-        }
-        return null;
-    };
-
-    public IconRecentsView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mActivity = BaseActivity.fromContext(context);
-        mContext = context;
-        mStatusBarForegroundScrim  =
-                Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
-        mTaskLoader = new TaskListLoader(mContext);
-        mTaskAdapter = new TaskAdapter(mTaskLoader);
-        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
-        mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter,
-                mActivity.getStatsLogManager());
-        mTaskAdapter.setActionController(mTaskActionController);
-        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
-        RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        if (mTaskRecyclerView == null) {
-            mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
-            mTaskRecyclerView.setAdapter(mTaskAdapter);
-            mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
-            ItemTouchHelper helper = new ItemTouchHelper(
-                    new TaskSwipeCallback(holder -> {
-                        mTaskActionController.removeTask(holder);
-                        if (mTaskLoader.getCurrentTaskList().isEmpty()) {
-                            mActivityHelper.leaveRecents();
-                        }
-                    }));
-            helper.attachToRecyclerView(mTaskRecyclerView);
-            mTaskRecyclerView.addOnChildAttachStateChangeListener(
-                    new OnChildAttachStateChangeListener() {
-                        @Override
-                        public void onChildViewAttachedToWindow(@NonNull View view) {
-                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
-                                // Child view was added that is not part of current layout animation
-                                // so restart the animation.
-                                animateFadeInLayoutAnimation();
-                            }
-                        }
-
-                        @Override
-                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
-                    });
-            mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
-            mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
-                    () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
-            ItemDecoration marginDecorator = new ItemDecoration() {
-                @Override
-                public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
-                        @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
-                    // TODO: Determine if current margins cause off screen item to be fully off
-                    // screen and if so, modify them so that it is partially off screen.
-                    int itemType = parent.getChildViewHolder(view).getItemViewType();
-                    Resources res = getResources();
-                    switch (itemType) {
-                        case ITEM_TYPE_CLEAR_ALL:
-                            outRect.top = (int) res.getDimension(
-                                    R.dimen.clear_all_item_view_top_margin);
-                            outRect.bottom = (int) res.getDimension(
-                                    R.dimen.clear_all_item_view_bottom_margin);
-                            break;
-                        case ITEM_TYPE_TASK:
-                            int desiredTopMargin = (int) res.getDimension(
-                                    R.dimen.task_item_top_margin);
-                            if (mTaskRecyclerView.getChildAdapterPosition(view) ==
-                                    state.getItemCount() - 1) {
-                                // Only add top margin to top task view if insets aren't enough.
-                                if (mInsets.top < desiredTopMargin) {
-                                    outRect.top = desiredTopMargin - mInsets.bottom;
-                                }
-                                return;
-                            }
-                            outRect.top = desiredTopMargin;
-                            break;
-                        default:
-                    }
-                }
-            };
-            mTaskRecyclerView.addItemDecoration(marginDecorator);
-
-            mEmptyView = findViewById(R.id.recent_task_empty_view);
-            mContentView = mTaskRecyclerView;
-            mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
-                @Override
-                public void onChanged() {
-                    updateContentViewVisibility();
-                }
-
-                @Override
-                public void onItemRangeRemoved(int positionStart, int itemCount) {
-                    updateContentViewVisibility();
-                }
-            });
-        }
-    }
-
-    @Override
-    public void setEnabled(boolean enabled) {
-        super.setEnabled(enabled);
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
-        }
-    }
-
-    /**
-     * Set activity helper for the view to callback to.
-     *
-     * @param helper the activity helper
-     */
-    public void setRecentsToActivityHelper(@NonNull RecentsToActivityHelper helper) {
-        mActivityHelper = helper;
-    }
-
-    /**
-     * Logic for when we know we are going to overview/recents and will be putting up the recents
-     * view. This should be used to prepare recents (e.g. load any task data, etc.) before it
-     * becomes visible.
-     */
-    public void onBeginTransitionToOverview() {
-        mStartedEnterAnimation = false;
-        if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
-            // all tasks should be visible to fill up the screen in portrait mode and the view will
-            // not be scrollable.
-            mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
-        }
-        if (!mUsingRemoteAnimation) {
-            scheduleFadeInLayoutAnimation();
-        }
-        // Load any task changes
-        if (!mTaskLoader.needsToLoad()) {
-            return;
-        }
-        mTaskAdapter.setIsShowingLoadingUi(true);
-        mTaskAdapter.notifyDataSetChanged();
-        mTaskLoader.loadTaskList(tasks -> {
-            int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-            mTaskAdapter.setIsShowingLoadingUi(false);
-            int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-            if (numEmptyItems < numActualItems) {
-                throw new IllegalStateException("There are less empty item views than the number "
-                        + "of items to animate to.");
-            }
-            // Possible that task list loads faster than adapter changes propagate to layout so
-            // only start content fill animation if there aren't any pending adapter changes and
-            // we've started the on enter layout animation.
-            boolean needsContentFillAnimation =
-                    !mTaskRecyclerView.hasPendingAdapterUpdates() && mStartedEnterAnimation;
-            if (needsContentFillAnimation) {
-                // Set item animator for content filling animation. The item animator will switch
-                // back to the default on completion
-                mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
-                mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
-                        numEmptyItems - numActualItems);
-                mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
-                        CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
-            } else {
-                // Notify change without animating.
-                mTaskAdapter.notifyDataSetChanged();
-            }
-        });
-    }
-
-    /**
-     * Set whether we transitioned to recents from the most recent app.
-     *
-     * @param transitionedFromApp true if transitioned from the most recent app, false otherwise
-     */
-    public void setTransitionedFromApp(boolean transitionedFromApp) {
-        mTransitionedFromApp = transitionedFromApp;
-    }
-
-    /**
-     * Set whether we're using a custom remote animation. If so, we will not do the default layout
-     * animation when entering recents and instead wait for the remote app surface to be ready to
-     * use.
-     *
-     * @param usingRemoteAnimation true if doing a remote animation, false o/w
-     */
-    public void setUsingRemoteAnimation(boolean usingRemoteAnimation) {
-        mUsingRemoteAnimation = usingRemoteAnimation;
-    }
-
-    /**
-     * Handles input from the overview button. Launch the most recent task unless we just came from
-     * the app. In that case, we launch the next most recent.
-     */
-    public void handleOverviewCommand() {
-        List<Task> tasks = mTaskLoader.getCurrentTaskList();
-        int tasksSize = tasks.size();
-        if (tasksSize == 0) {
-            // Do nothing
-            return;
-        }
-        Task taskToLaunch;
-        if (mTransitionedFromApp && tasksSize > 1) {
-            // Launch the next most recent app
-            taskToLaunch = tasks.get(1);
-        } else {
-            // Launch the most recent app
-            taskToLaunch = tasks.get(0);
-        }
-
-        // See if view for this task is attached, and if so, animate launch from that view.
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView taskView = itemViews.get(i);
-            TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
-            if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) {
-                mTaskActionController.launchTaskFromView(holder);
-                return;
-            }
-        }
-
-        // Otherwise, just use a basic launch animation.
-        mTaskActionController.launchTask(taskToLaunch);
-    }
-
-    /**
-     * Set whether or not to show the scrim in between the view and the top insets. This only works
-     * if the view is being insetted in the first place.
-     *
-     * The scrim is added to the activity's root view to prevent animations on this view
-     * affecting the scrim. As a result, it is the activity's responsibility to show/hide this
-     * scrim as appropriate.
-     *
-     * @param showStatusBarForegroundScrim true to show the scrim, false to hide
-     */
-    public void setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim) {
-        mShowStatusBarForegroundScrim = showStatusBarForegroundScrim;
-        if (mShowStatusBarForegroundScrim != showStatusBarForegroundScrim) {
-            updateStatusBarScrim();
-        }
-    }
-
-    private void updateStatusBarScrim() {
-        boolean shouldShow = mInsets.top != 0 && mShowStatusBarForegroundScrim;
-        mActivity.getDragLayer().setForeground(shouldShow ? mStatusBarForegroundScrim : null);
-    }
-
-    /**
-     * Get the bottom most task view to animate to.
-     *
-     * @return the task view
-     */
-    private @Nullable TaskItemView getBottomTaskView() {
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View view = mTaskRecyclerView.getChildAt(i);
-            if (mTaskRecyclerView.getChildViewHolder(view).getItemViewType() == ITEM_TYPE_TASK) {
-                return (TaskItemView) view;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Whether this view has processed all data changes and is ready to animate from the app to
-     * the overview.
-     *
-     * @return true if ready to animate app to overview, false otherwise
-     */
-    public boolean isReadyForRemoteAnim() {
-        return !mTaskRecyclerView.hasPendingAdapterUpdates();
-    }
-
-    /**
-     * Set a callback for whenever this view is ready to do a remote animation from the app to
-     * overview. See {@link #isReadyForRemoteAnim()}.
-     *
-     * @param callback callback to run when view is ready to animate
-     */
-    public void setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback) {
-        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        if (isReadyForRemoteAnim()) {
-                            callback.onReadyForRemoteAnim();
-                            mTaskRecyclerView.getViewTreeObserver().
-                                    removeOnGlobalLayoutListener(this);
-                        }
-                    }
-                });
-    }
-
-    /**
-     * Clear all tasks and animate out.
-     */
-    private void animateClearAllTasks() {
-        setEnabled(false);
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-
-        AnimatorSet clearAnim = new AnimatorSet();
-        long currentDelay = 0;
-
-        // Animate each item view to the right and fade out.
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView itemView = itemViews.get(i);
-            PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
-                    0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
-            PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
-            ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
-                    transXproperty, alphaProperty);
-            itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
-            itemAnim.setStartDelay(currentDelay);
-
-            clearAnim.play(itemAnim);
-            currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
-        }
-
-        // Animate view fading and leave recents when faded enough.
-        ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
-                .setDuration(CROSSFADE_DURATION);
-        contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
-        contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            private boolean mLeftRecents = false;
-
-            @Override
-            public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
-                // Leave recents while fading out.
-                if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
-                    mActivityHelper.leaveRecents();
-                    mLeftRecents = true;
-                }
-            }
-        });
-
-        clearAnim.play(contentAlpha);
-        clearAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                for (int i = 0, size = itemViews.size(); i < size; i++) {
-                    TaskItemView itemView = itemViews.get(i);
-                    itemView.setTranslationX(0);
-                    itemView.setAlpha(1.0f);
-                }
-                setEnabled(true);
-                mContentView.setVisibility(GONE);
-                mTaskActionController.clearAllTasks();
-            }
-        });
-        clearAnim.start();
-    }
-
-    /**
-     * Get attached task item views ordered by most recent.
-     *
-     * @return array list of attached task item views
-     */
-    private ArrayList<TaskItemView> getTaskViews() {
-        int taskCount = mTaskRecyclerView.getChildCount();
-        ArrayList<TaskItemView> itemViews = new ArrayList<>();
-        for (int i = 0; i < taskCount; i ++) {
-            View child = mTaskRecyclerView.getChildAt(i);
-            if (child instanceof TaskItemView) {
-                itemViews.add((TaskItemView) child);
-            }
-        }
-        return itemViews;
-    }
-
-    /**
-     * Update the content view so that the appropriate view is shown based off the current list
-     * of tasks.
-     */
-    private void updateContentViewVisibility() {
-        int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-        if (mShowingContentView != mEmptyView && taskListSize == 0) {
-            mShowingContentView = mEmptyView;
-            crossfadeViews(mEmptyView, mContentView);
-        }
-        if (mShowingContentView != mContentView && taskListSize > 0) {
-            mShowingContentView = mContentView;
-            crossfadeViews(mContentView, mEmptyView);
-        }
-    }
-
-    /**
-     * Animate views so that one view fades in while the other fades out.
-     *
-     * @param fadeInView view that should fade in
-     * @param fadeOutView view that should fade out
-     */
-    private void crossfadeViews(View fadeInView, View fadeOutView) {
-        fadeInView.animate().cancel();
-        fadeInView.setVisibility(VISIBLE);
-        fadeInView.setAlpha(0f);
-        fadeInView.animate()
-                .alpha(1f)
-                .setDuration(CROSSFADE_DURATION)
-                .setListener(null);
-
-        fadeOutView.animate().cancel();
-        fadeOutView.animate()
-                .alpha(0f)
-                .setDuration(CROSSFADE_DURATION)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        fadeOutView.setVisibility(GONE);
-                    }
-                });
-    }
-
-    /**
-     * Schedule a one-shot layout animation on the next layout. Separate from
-     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
-     * view properties themselves, allowing more controllable behavior and making it easier to
-     * manage when the animation conflicts with another animation.
-     */
-    private void scheduleFadeInLayoutAnimation() {
-        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        animateFadeInLayoutAnimation();
-                        mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                    }
-                });
-    }
-
-    /**
-     * Start animating the layout animation where items fade in.
-     */
-    private void animateFadeInLayoutAnimation() {
-        if (mLayoutAnimation != null) {
-            // If layout animation still in progress, cancel and restart.
-            mLayoutAnimation.cancel();
-        }
-        ArrayList<TaskItemView> views = getTaskViews();
-        int delay = 0;
-        mLayoutAnimation = new AnimatorSet();
-        for (int i = 0, size = views.size(); i < size; i++) {
-            TaskItemView view = views.get(i);
-            view.setAlpha(0.0f);
-            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
-            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
-            alphaAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    view.setAlpha(1.0f);
-                    mLayingOutViews.remove(view);
-                }
-            });
-            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
-            mLayoutAnimation.play(alphaAnim);
-            mLayingOutViews.add(view);
-        }
-        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mLayoutAnimation = null;
-            }
-        });
-        mLayoutAnimation.start();
-        mStartedEnterAnimation = true;
-    }
-
-    /**
-     * Play remote app to recents animation when the app is the home activity. We use a simple
-     * cross-fade here. Note this is only used if the home activity is a separate app than the
-     * recents activity.
-     *
-     * @param anim animator set
-     * @param homeTarget the home surface thats closing
-     * @param recentsTarget the surface containing recents
-     */
-    public void playRemoteHomeToRecentsAnimation(@NonNull AnimatorSet anim,
-            @NonNull RemoteAnimationTargetCompat homeTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(this);
-
-        SurfaceParams[] params = new SurfaceParams[2];
-        int boostedMode = MODE_CLOSING;
-
-        ValueAnimator remoteHomeAnim = ValueAnimator.ofFloat(0, 1);
-        remoteHomeAnim.setDuration(REMOTE_APP_TO_OVERVIEW_DURATION);
-
-        remoteHomeAnim.addUpdateListener(valueAnimator -> {
-            float val = (float) valueAnimator.getAnimatedValue();
-            float alpha;
-            RemoteAnimationTargetCompat visibleTarget;
-            RemoteAnimationTargetCompat invisibleTarget;
-            if (val < .5f) {
-                visibleTarget = homeTarget;
-                invisibleTarget = recentsTarget;
-                alpha = 1 - (val * 2);
-            } else {
-                visibleTarget = recentsTarget;
-                invisibleTarget = homeTarget;
-                alpha = (val - .5f) * 2;
-            }
-            params[0] = new SurfaceParams(visibleTarget.leash, alpha, null /* matrix */,
-                    null /* windowCrop */, getLayer(visibleTarget, boostedMode),
-                    0 /* cornerRadius */);
-            params[1] = new SurfaceParams(invisibleTarget.leash, 0.0f, null /* matrix */,
-                    null /* windowCrop */, getLayer(invisibleTarget, boostedMode),
-                    0 /* cornerRadius */);
-            surfaceApplier.scheduleApply(params);
-        });
-        anim.play(remoteHomeAnim);
-        animateFadeInLayoutAnimation();
-    }
-
-    /**
-     * Play remote animation from app to recents. This should scale the currently closing app down
-     * to the recents thumbnail.
-     *
-     * @param anim animator set
-     * @param appTarget the app surface thats closing
-     * @param recentsTarget the surface containing recents
-     */
-    public void playRemoteAppToRecentsAnimation(@NonNull AnimatorSet anim,
-            @NonNull RemoteAnimationTargetCompat appTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget) {
-        TaskItemView bottomView = getBottomTaskView();
-        if (bottomView == null) {
-            // This can be null if there were previously 0 tasks and the recycler view has not had
-            // enough time to take in the data change, bind a new view, and lay out the new view.
-            // TODO: Have a fallback to animate to
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(REMOTE_APP_TO_OVERVIEW_DURATION));
-            return;
-        }
-        final Matrix appMatrix = new Matrix();
-        playRemoteTransYAnim(anim, appMatrix);
-        playRemoteAppScaleDownAnim(anim, appMatrix, appTarget, recentsTarget,
-                bottomView.getThumbnailView());
-        playRemoteTaskListFadeIn(anim, bottomView);
-        mStartedEnterAnimation = true;
-    }
-
-    /**
-     * Play translation Y animation for the remote app to recents animation. Animates over all task
-     * views as well as the closing app, easing them into their final vertical positions.
-     *
-     * @param anim animator set to play on
-     * @param appMatrix transformation matrix for the closing app surface
-     */
-    private void playRemoteTransYAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix) {
-        final ArrayList<TaskItemView> views = getTaskViews();
-
-        // Start Y translation from about halfway through the tasks list to the bottom thumbnail.
-        float taskHeight = getResources().getDimension(R.dimen.task_item_height);
-        float totalTransY = -(MAX_TASKS_TO_DISPLAY / 2.0f - 1) * taskHeight;
-        for (int i = 0, size = views.size(); i < size; i++) {
-            views.get(i).setTranslationY(totalTransY);
-        }
-
-        ValueAnimator transYAnim = ValueAnimator.ofFloat(totalTransY, 0);
-        transYAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
-        transYAnim.setInterpolator(FAST_OUT_SLOW_IN_2);
-        transYAnim.addUpdateListener(valueAnimator -> {
-            float transY = (float) valueAnimator.getAnimatedValue();
-            for (int i = 0, size = views.size(); i < size; i++) {
-                views.get(i).setTranslationY(transY);
-            }
-            appMatrix.postTranslate(0, transY - totalTransY);
-        });
-        transYAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                for (int i = 0, size = views.size(); i < size; i++) {
-                    views.get(i).setTranslationY(0);
-                }
-            }
-        });
-        anim.play(transYAnim);
-    }
-
-    /**
-     * Play the scale down animation for the remote app to recents animation where the app surface
-     * scales down to where the thumbnail is.
-     *
-     * @param anim animator set to play on
-     * @param appMatrix transformation matrix for the app surface
-     * @param appTarget closing app target
-     * @param recentsTarget opening recents target
-     * @param thumbnailView thumbnail view to animate to
-     */
-    private void playRemoteAppScaleDownAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix,
-            @NonNull RemoteAnimationTargetCompat appTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget,
-            @NonNull View thumbnailView) {
-        // Identify where the entering remote app should animate to.
-        Rect endRect = new Rect();
-        thumbnailView.getGlobalVisibleRect(endRect);
-        Rect appBounds = appTarget.sourceContainerBounds;
-        RectF currentAppRect = new RectF();
-
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(this);
-
-        // Keep recents visible throughout the animation.
-        SurfaceParams[] params = new SurfaceParams[2];
-        // Closing app should stay on top.
-        int boostedMode = MODE_CLOSING;
-        params[0] = new SurfaceParams(recentsTarget.leash, 1f, null /* matrix */,
-                null /* windowCrop */, getLayer(recentsTarget, boostedMode), 0 /* cornerRadius */);
-
-        ValueAnimator remoteAppAnim = ValueAnimator.ofInt(0, 1);
-        remoteAppAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
-        remoteAppAnim.addUpdateListener(new MultiValueUpdateListener() {
-            private final FloatProp mScaleX;
-            private final FloatProp mScaleY;
-            private final FloatProp mTranslationX;
-            private final FloatProp mTranslationY;
-            private final FloatProp mAlpha;
-
-            {
-                // Scale down and move to view location.
-                float endScaleX = ((float) endRect.width()) / appBounds.width();
-                mScaleX = new FloatProp(1f, endScaleX, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        FAST_OUT_SLOW_IN_1);
-                float endScaleY = ((float) endRect.height()) / appBounds.height();
-                mScaleY = new FloatProp(1f, endScaleY, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        FAST_OUT_SLOW_IN_1);
-                float endTranslationX = endRect.left -
-                        (appBounds.width() - thumbnailView.getWidth()) / 2.0f;
-                mTranslationX = new FloatProp(0, endTranslationX, 0,
-                        REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_1);
-                float endTranslationY = endRect.top -
-                        (appBounds.height() - thumbnailView.getHeight()) / 2.0f;
-                mTranslationY = new FloatProp(0, endTranslationY, 0,
-                        REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_2);
-                mAlpha = new FloatProp(1.0f, 0, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        ACCEL_2);
-            }
-
-            @Override
-            public void onUpdate(float percent) {
-                Matrix m = new Matrix();
-                m.preScale(mScaleX.value, mScaleY.value,
-                        appBounds.width() / 2.0f, appBounds.height() / 2.0f);
-                m.postTranslate(mTranslationX.value, mTranslationY.value);
-                appMatrix.preConcat(m);
-                params[1] = new SurfaceParams(appTarget.leash, mAlpha.value, appMatrix,
-                        null /* windowCrop */, getLayer(appTarget, boostedMode),
-                        0 /* cornerRadius */);
-                surfaceApplier.scheduleApply(params);
-
-                m.mapRect(currentAppRect, new RectF(appBounds));
-                setViewToRect(thumbnailView, new RectF(endRect), currentAppRect);
-                appMatrix.reset();
-            }
-        });
-        remoteAppAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                thumbnailView.setTranslationY(0);
-                thumbnailView.setTranslationX(0);
-                thumbnailView.setScaleX(1);
-                thumbnailView.setScaleY(1);
-            }
-        });
-        anim.play(remoteAppAnim);
-    }
-
-    /**
-     * Play task list fade in animation as part of remote app to recents animation. This animation
-     * ensures that the task views in the recents list fade in from bottom to top.
-     *
-     * @param anim animator set to play on
-     * @param appTaskView the task view associated with the remote app closing
-     */
-    private void playRemoteTaskListFadeIn(@NonNull AnimatorSet anim,
-            @NonNull TaskItemView appTaskView) {
-        long delay = REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY;
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            ValueAnimator fadeAnim = ValueAnimator.ofFloat(0, 1.0f);
-            fadeAnim.setDuration(REMOTE_TO_RECENTS_ITEM_FADE_DURATION).setInterpolator(OUT_SLOW_IN);
-            fadeAnim.setStartDelay(delay);
-            View view = mTaskRecyclerView.getChildAt(i);
-            if (Objects.equals(view, appTaskView)) {
-                // Only animate icon and text for the view with snapshot animating in
-                final View icon = appTaskView.getIconView();
-                final View label = appTaskView.getLabelView();
-
-                icon.setAlpha(0.0f);
-                label.setAlpha(0.0f);
-
-                fadeAnim.addUpdateListener(alphaVal -> {
-                    float val = alphaVal.getAnimatedFraction();
-
-                    icon.setAlpha(val);
-                    label.setAlpha(val);
-                });
-                fadeAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        icon.setAlpha(1.0f);
-                        label.setAlpha(1.0f);
-                    }
-                });
-            } else {
-                // Otherwise, fade in the entire view.
-                view.setAlpha(0.0f);
-                fadeAnim.addUpdateListener(alphaVal -> {
-                    float val = alphaVal.getAnimatedFraction();
-                    view.setAlpha(val);
-                });
-                fadeAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        view.setAlpha(1.0f);
-                    }
-                });
-            }
-            anim.play(fadeAnim);
-
-            int itemType = mTaskRecyclerView.getChildViewHolder(view).getItemViewType();
-            if (itemType == ITEM_TYPE_CLEAR_ALL) {
-                // Don't add delay. Clear all should animate at same time as next view.
-                continue;
-            }
-            delay += REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY;
-        }
-    }
-
-    /**
-     * Set view properties so that the view fits to the target rect.
-     *
-     * @param view view to set
-     * @param origRect original rect that view was located
-     * @param targetRect rect to set to
-     */
-    private void setViewToRect(View view, RectF origRect, RectF targetRect) {
-        float dX = targetRect.left - origRect.left;
-        float dY = targetRect.top - origRect.top;
-        view.setTranslationX(dX);
-        view.setTranslationY(dY);
-
-        float scaleX = targetRect.width() / origRect.width();
-        float scaleY = targetRect.height() / origRect.height();
-        view.setPivotX(0);
-        view.setPivotY(0);
-        view.setScaleX(scaleX);
-        view.setScaleY(scaleY);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets = insets;
-        mTaskRecyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
-        mTaskRecyclerView.invalidateItemDecorations();
-        if (mInsets.top != 0) {
-            updateStatusBarScrim();
-        }
-    }
-
-    /**
-     * Callback for when this view is ready for a remote animation from app to overview.
-     */
-    public interface onReadyForRemoteAnimCallback {
-
-        void onReadyForRemoteAnim();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
deleted file mode 100644
index f184ad0..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.quickstep.ThumbnailDrawable;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-
-/**
- * View representing an individual task item with the icon + thumbnail adjacent to the task label.
- */
-public final class TaskItemView extends LinearLayout {
-
-    private static final String EMPTY_LABEL = "";
-    private static final String DEFAULT_LABEL = "...";
-    private final Drawable mDefaultIcon;
-    private final Drawable mDefaultThumbnail;
-    private final TaskLayerDrawable mIconDrawable;
-    private final TaskLayerDrawable mThumbnailDrawable;
-    private View mTaskIconThumbnailView;
-    private TextView mLabelView;
-    private ImageView mIconView;
-    private ImageView mThumbnailView;
-    private float mContentTransitionProgress;
-    private int mDisplayedOrientation;
-
-    /**
-     * Property representing the content transition progress of the view. 1.0f represents that the
-     * currently bound icon, thumbnail, and label are fully animated in and visible.
-     */
-    public static FloatProperty CONTENT_TRANSITION_PROGRESS =
-            new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
-                @Override
-                public void setValue(TaskItemView view, float progress) {
-                    view.setContentTransitionProgress(progress);
-                }
-
-                @Override
-                public Float get(TaskItemView view) {
-                    return view.mContentTransitionProgress;
-                }
-            };
-
-    public TaskItemView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        Resources res = context.getResources();
-        mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
-        mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
-        mIconDrawable = new TaskLayerDrawable(context);
-        mThumbnailDrawable = new TaskLayerDrawable(context);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mLabelView = findViewById(R.id.task_label);
-        mTaskIconThumbnailView = findViewById(R.id.task_icon_and_thumbnail);
-        mThumbnailView = findViewById(R.id.task_thumbnail);
-        mIconView = findViewById(R.id.task_icon);
-
-        mThumbnailView.setImageDrawable(mThumbnailDrawable);
-        mIconView.setImageDrawable(mIconDrawable);
-
-        resetToEmptyUi();
-        CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
-    }
-
-    /**
-     * Resets task item view to empty, loading UI.
-     */
-    public void resetToEmptyUi() {
-        mIconDrawable.resetDrawable();
-        mThumbnailDrawable.resetDrawable();
-        setLabel(EMPTY_LABEL);
-    }
-
-    /**
-     * Set the label for the task item. Sets to a default label if null.
-     *
-     * @param label task label
-     */
-    public void setLabel(@Nullable String label) {
-        mLabelView.setText(getSafeLabel(label));
-        // TODO: Animation for label
-    }
-
-    /**
-     * Set the icon for the task item. Sets to a default icon if null.
-     *
-     * @param icon task icon
-     */
-    public void setIcon(@Nullable Drawable icon) {
-        // TODO: Scale the icon up based off the padding on the side
-        // The icon proper is actually smaller than the drawable and has "padding" on the side for
-        // the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
-        // view if we want the icon to be flush with the bottom of the thumbnail.
-        mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
-    }
-
-    /**
-     * Set the task thumbnail for the task. Sets to a default thumbnail if null.
-     *
-     * @param thumbnailData task thumbnail data for the task
-     */
-    public void setThumbnail(@Nullable ThumbnailData thumbnailData) {
-        mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnailData));
-    }
-
-    public View getThumbnailView() {
-        return mThumbnailView;
-    }
-
-    public View getIconView() {
-        return mIconView;
-    }
-
-    public View getLabelView() {
-        return mLabelView;
-    }
-
-    /**
-     * Start a new animation from the current task content to the specified new content. The caller
-     * is responsible for the actual animation control via the property
-     * {@link #CONTENT_TRANSITION_PROGRESS}.
-     *
-     * @param endIcon the icon to animate to
-     * @param endThumbnail the thumbnail to animate to
-     * @param endLabel the label to animate to
-     */
-    public void startContentAnimation(@Nullable Drawable endIcon,
-            @Nullable ThumbnailData endThumbnail, @Nullable String endLabel) {
-        mIconDrawable.startNewTransition(getSafeIcon(endIcon));
-        mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
-        // TODO: Animation for label
-
-        setContentTransitionProgress(0.0f);
-    }
-
-    private void setContentTransitionProgress(float progress) {
-        mContentTransitionProgress = progress;
-        mIconDrawable.setTransitionProgress(progress);
-        mThumbnailDrawable.setTransitionProgress(progress);
-        // TODO: Animation for label
-    }
-
-    private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
-        return (icon != null) ? icon : mDefaultIcon;
-    }
-
-    private @NonNull Drawable getSafeThumbnail(@Nullable ThumbnailData thumbnailData) {
-        if (thumbnailData == null || thumbnailData.thumbnail == null) {
-            return mDefaultThumbnail;
-        }
-        int orientation = getResources().getConfiguration().orientation;
-        return new ThumbnailDrawable(getResources(), thumbnailData,
-                orientation /* requestedOrientation */);
-    }
-
-    private @NonNull String getSafeLabel(@Nullable String label) {
-        return (label != null) ? label : DEFAULT_LABEL;
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        onOrientationChanged(getResources().getConfiguration().orientation);
-    }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        onOrientationChanged(newConfig.orientation);
-    }
-
-    private void onOrientationChanged(int newOrientation) {
-        if (mDisplayedOrientation == newOrientation) {
-            return;
-        }
-        mDisplayedOrientation = newOrientation;
-        int layerCount = mThumbnailDrawable.getNumberOfLayers();
-        for (int i = 0; i < layerCount; i++) {
-            Drawable drawable = mThumbnailDrawable.getDrawable(i);
-            if (drawable instanceof ThumbnailDrawable) {
-                ((ThumbnailDrawable) drawable).setRequestedOrientation(newOrientation);
-            }
-        }
-        mTaskIconThumbnailView.forceLayout();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
deleted file mode 100644
index 98b66b9..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.R;
-
-/**
- * A layer drawable for task content that transitions between two drawables by crossfading. Similar
- * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
- * progress and provides a default, empty drawable.
- */
-public final class TaskLayerDrawable extends LayerDrawable {
-    private final Drawable mEmptyDrawable;
-    private float mProgress;
-
-    public TaskLayerDrawable(Context context) {
-        super(new Drawable[0]);
-
-        // Use empty drawable for both layers initially.
-        mEmptyDrawable = context.getResources().getDrawable(
-                R.drawable.empty_content_box, context.getTheme());
-        addLayer(mEmptyDrawable);
-        addLayer(mEmptyDrawable);
-        setTransitionProgress(1.0f);
-    }
-
-    /**
-     * Immediately set the front-most drawable layer.
-     *
-     * @param drawable drawable to set
-     */
-    public void setCurrentDrawable(@NonNull Drawable drawable) {
-        setDrawable(0, drawable);
-        applyTransitionProgress(mProgress);
-    }
-
-    /**
-     * Immediately reset the drawable to showing the empty drawable.
-     */
-    public void resetDrawable() {
-        setCurrentDrawable(mEmptyDrawable);
-    }
-
-    /**
-     * Prepare to start animating the transition by pushing the current drawable to the back and
-     * setting a new drawable to the front layer and making it invisible.
-     *
-     * @param endDrawable drawable to animate to
-     */
-    public void startNewTransition(@NonNull Drawable endDrawable) {
-        Drawable oldDrawable = getDrawable(0);
-        setDrawable(1, oldDrawable);
-        setDrawable(0, endDrawable);
-        setTransitionProgress(0.0f);
-    }
-
-    /**
-     * Set the progress of the transition animation to crossfade the two drawables.
-     *
-     * @param progress current transition progress between 0 (front view invisible) and 1
-     *                 (front view visible)
-     */
-    public void setTransitionProgress(float progress) {
-        if (progress > 1 || progress < 0) {
-            throw new IllegalArgumentException("Transition progress should be between 0 and 1");
-        }
-        mProgress = progress;
-        applyTransitionProgress(progress);
-    }
-
-    private void applyTransitionProgress(float progress) {
-        int drawableAlpha = (int) (progress * 255);
-        getDrawable(0).setAlpha(drawableAlpha);
-        if (getDrawable(0) != getDrawable(1)) {
-            // Only do this if it's a different drawable so that it fades out.
-            // Otherwise, we'd just be overwriting the front drawable's alpha.
-            getDrawable(1).setAlpha(255 - drawableAlpha);
-        }
-        invalidateSelf();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
deleted file mode 100644
index eaefa21..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.launcher3.R;
-
-/**
- * Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
- * within the view. Side length is determined by height.
- */
-final class TaskThumbnailIconView extends ViewGroup {
-    private final Rect mTmpFrameRect = new Rect();
-    private final Rect mTmpChildRect = new Rect();
-    private View mThumbnailView;
-    private View mIconView;
-    private static final float SUBITEM_FRAME_RATIO = .6f;
-
-    public TaskThumbnailIconView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mThumbnailView = findViewById(R.id.task_thumbnail);
-        mIconView = findViewById(R.id.task_icon);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
-        int width = height;
-        setMeasuredDimension(width, height);
-
-        int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
-        if (mThumbnailView.getVisibility() != GONE) {
-            boolean isPortrait =
-                    (getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT);
-            int thumbnailHeightSpec =
-                    makeMeasureSpec(isPortrait ? height : subItemSize, MeasureSpec.EXACTLY);
-            int thumbnailWidthSpec =
-                    makeMeasureSpec(isPortrait ? subItemSize : width, MeasureSpec.EXACTLY);
-            measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
-        }
-        if (mIconView.getVisibility() != GONE) {
-            int iconHeightSpec = makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
-            int iconWidthSpec = makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
-            measureChild(mIconView, iconWidthSpec, iconHeightSpec);
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        mTmpFrameRect.left = getPaddingLeft();
-        mTmpFrameRect.right = right - left - getPaddingRight();
-        mTmpFrameRect.top = getPaddingTop();
-        mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
-
-        // Layout the thumbnail to the top-start corner of the view
-        if (mThumbnailView.getVisibility() != GONE) {
-            final int width = mThumbnailView.getMeasuredWidth();
-            final int height = mThumbnailView.getMeasuredHeight();
-
-            final int thumbnailGravity = Gravity.TOP | Gravity.START;
-            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
-
-            mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
-                    mTmpChildRect.right, mTmpChildRect.bottom);
-        }
-
-        // Layout the icon to the bottom-end corner of the view
-        if (mIconView.getVisibility() != GONE) {
-            final int width = mIconView.getMeasuredWidth();
-            final int height = mIconView.getMeasuredHeight();
-
-            int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
-            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
-
-            mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
-                    mTmpChildRect.right, mTmpChildRect.bottom);
-        }
-    }
-}
diff --git a/go/res/values-v26/bools.xml b/go/res/values-v26/bools.xml
index cc4a7ba..1584734 100644
--- a/go/res/values-v26/bools.xml
+++ b/go/res/values-v26/bools.xml
@@ -17,5 +17,6 @@
 -->
 
 <resources>
-    <bool name="notification_badging_enabled">false</bool>
-</resources>
\ No newline at end of file
+    <bool name="notification_dots_enabled">false</bool>
+</resources>
+
diff --git a/go/res/xml/device_profiles.xml b/go/res/xml/device_profiles.xml
index 0fd0eeb..0c7eba3 100644
--- a/go/res/xml/device_profiles.xml
+++ b/go/res/xml/device_profiles.xml
@@ -24,6 +24,7 @@
         launcher:numFolderRows="4"
         launcher:numFolderColumns="4"
         launcher:numHotseatIcons="4"
+        launcher:dbFile="launcher.db"
         launcher:defaultLayoutId="@xml/default_workspace_4x4" >
 
         <display-option
@@ -36,4 +37,4 @@
 
     </grid-option>
 
-</profiles>
\ No newline at end of file
+</profiles>
diff --git a/go/src/com/android/launcher3/config/FeatureFlags.java b/go/src/com/android/launcher3/config/FeatureFlags.java
deleted file mode 100644
index a90808c..0000000
--- a/go/src/com/android/launcher3/config/FeatureFlags.java
+++ /dev/null
@@ -1,32 +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.config;
-
-import android.content.Context;
-
-/**
- * Defines a set of flags used to control various launcher behaviors
- */
-public final class FeatureFlags extends BaseFlags {
-    private FeatureFlags() {
-        // Prevent instantiation
-    }
-
-    // Features to control Launcher3Go behavior
-    public static final boolean GO_DISABLE_WIDGETS = true;
-    public static final boolean LAUNCHER3_SPRING_ICONS = false;
-}
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 26c3313..7130531 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,10 +16,11 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
+import com.android.launcher3.util.LooperExecutor;
 
 /**
  * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -27,8 +28,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 18f3f9d..3b3dc01 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -16,11 +16,14 @@
 
 package com.android.launcher3.model;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.UserHandle;
 
-import com.android.launcher3.icons.ComponentWithLabel;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
@@ -29,14 +32,16 @@
 import java.util.List;
 import java.util.Set;
 
-import androidx.annotation.Nullable;
-
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
  *
  * <p> The widgets and shortcuts are organized using package name as its index.
  */
 public class WidgetsModel {
+
+    // True is the widget support is disabled.
+    public static final boolean GO_DISABLE_WIDGETS = true;
+
     private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
 
     /**
@@ -55,7 +60,7 @@
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
      */
-    public List<ComponentWithLabel> update(LauncherAppState app,
+    public List<ComponentWithLabelAndIcon> update(LauncherAppState app,
             @Nullable PackageUserKey packageUser) {
         return Collections.emptyList();
     }
@@ -64,4 +69,9 @@
     public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
             LauncherAppState app) {
     }
+
+    public WidgetItem getWidgetProviderInfoByProviderName(
+            ComponentName providerName) {
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 42b1194..0000000
--- a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shortcuts;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.notification.NotificationKeyData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
-
-    private static final DeepShortcutManager sInstance = new DeepShortcutManager();
-
-    public static DeepShortcutManager getInstance(Context context) {
-        return sInstance;
-    }
-
-    private final QueryResult mFailure = new QueryResult();
-
-    private DeepShortcutManager() { }
-
-    /**
-     * Queries for the shortcuts with the package name and provided ids.
-     *
-     * This method is intended to get the full details for shortcuts when they are added or updated,
-     * because we only get "key" fields in onShortcutsChanged().
-     */
-    public QueryResult queryForFullDetails(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return mFailure;
-    }
-
-    /**
-     * Gets all the manifest and dynamic shortcuts associated with the given package and user,
-     * to be displayed in the shortcuts container on long press.
-     */
-    public QueryResult queryForShortcutsContainer(ComponentName activity,
-            UserHandle user) {
-        return mFailure;
-    }
-
-    /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void unpinShortcut(final ShortcutKey key) {
-    }
-
-    /**
-     * Adds the given shortcut to the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void pinShortcut(final ShortcutKey key) {
-    }
-
-    public void startShortcut(String packageName, String id, Rect sourceBounds,
-            Bundle startActivityOptions, UserHandle user) {
-    }
-
-    public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
-        return null;
-    }
-
-    /**
-     * Returns the id's of pinned shortcuts associated with the given package and user.
-     *
-     * If packageName is null, returns all pinned shortcuts regardless of package.
-     */
-    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
-        return mFailure;
-    }
-
-    public QueryResult queryForPinnedShortcuts(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return mFailure;
-    }
-
-    public QueryResult queryForAllShortcuts(UserHandle user) {
-        return mFailure;
-    }
-
-    public boolean hasHostPermission() {
-        return false;
-    }
-
-
-    public static class QueryResult extends ArrayList<ShortcutInfo> {
-
-        public boolean wasSuccess() {
-            return true;
-        }
-    }
-}
diff --git a/gradle.properties b/gradle.properties
index 5b90f08..7a51375 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,12 +2,12 @@
 android.useAndroidX = true
 android.enableJetifier = true
 
-ANDROID_X_VERSION=1.0.0-beta01
+ANDROID_X_VERSION=1+
 
-GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.3.0
+GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.5.1
 
-PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.6
+PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.8
 PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
 
 BUILD_TOOLS_VERSION=28.0.3
-COMPILE_SDK=android-Q
\ No newline at end of file
+COMPILE_SDK=android-R
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
deleted file mode 100644
index f12d16e..0000000
--- a/iconloaderlib/Android.bp
+++ /dev/null
@@ -1,44 +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.
-
-android_library {
-    name: "iconloader_base",
-    sdk_version: "28",
-    min_sdk_version: "21",
-    static_libs: [
-        "androidx.core_core",
-    ],
-    resource_dirs: [
-        "res",
-    ],
-    srcs: [
-        "src/**/*.java",
-    ],
-}
-
-android_library {
-    name: "iconloader",
-    sdk_version: "system_current",
-    min_sdk_version: "21",
-    static_libs: [
-        "androidx.core_core",
-    ],
-    resource_dirs: [
-        "res",
-    ],
-    srcs: [
-        "src/**/*.java",
-        "src_full_lib/**/*.java",
-    ],
-}
diff --git a/iconloaderlib/AndroidManifest.xml b/iconloaderlib/AndroidManifest.xml
deleted file mode 100644
index b30258d..0000000
--- a/iconloaderlib/AndroidManifest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.launcher3.icons">
-</manifest>
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
deleted file mode 100644
index 8a4d2b7..0000000
--- a/iconloaderlib/build.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
-    compileSdkVersion COMPILE_SDK
-    buildToolsVersion BUILD_TOOLS_VERSION
-    publishNonDefault true
-
-    defaultConfig {
-        minSdkVersion 25
-        targetSdkVersion 28
-        versionCode 1
-        versionName "1.0"
-    }
-
-    sourceSets {
-        main {
-            java.srcDirs = ['src', 'src_full_lib']
-            manifest.srcFile 'AndroidManifest.xml'
-            res.srcDirs = ['res']
-        }
-    }
-
-    lintOptions {
-        abortOnError false
-    }
-
-    tasks.withType(JavaCompile) {
-        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
-    }
-
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-}
-
-dependencies {
-    implementation "androidx.core:core:${ANDROID_X_VERSION}"
-}
diff --git a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
deleted file mode 100644
index 9f13cf5..0000000
--- a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
+++ /dev/null
@@ -1,22 +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.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/legacy_icon_background"/>
-    <foreground>
-        <com.android.launcher3.icons.FixedScaleDrawable />
-    </foreground>
-</adaptive-icon>
diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
deleted file mode 100644
index b74317e..0000000
--- a/iconloaderlib/res/drawable/ic_instant_app_badge.xml
+++ /dev/null
@@ -1,39 +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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/profile_badge_size"
-    android:height="@dimen/profile_badge_size"
-    android:viewportWidth="18"
-    android:viewportHeight="18">
-
-    <path
-        android:fillColor="@android:color/black"
-        android:strokeWidth="1"
-        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:strokeWidth="1"
-        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:strokeWidth="1"
-        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
-    <path
-        android:fillColor="@android:color/black"
-        android:fillAlpha="0.87"
-        android:strokeWidth="1"
-        android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
-</vector>
diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml
deleted file mode 100644
index 873b2fc..0000000
--- a/iconloaderlib/res/values/colors.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 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.
-*/
--->
-<resources>
-    <color name="legacy_icon_background">#FFFFFF</color>
-</resources>
diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml
deleted file mode 100644
index 68c2d2e..0000000
--- a/iconloaderlib/res/values/config.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 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.
-*/
--->
-<resources>
-
-    <!-- Various configurations to control the simple cache implementation -->
-
-    <dimen name="default_icon_bitmap_size">56dp</dimen>
-    <bool name="simple_cache_enable_im_memory">false</bool>
-    <string name="cache_db_name" translatable="false">app_icons.db</string>
-
-</resources>
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
deleted file mode 100644
index 5c4f37c..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ /dev/null
@@ -1,365 +0,0 @@
-package com.android.launcher3.icons;
-
-import static android.graphics.Paint.DITHER_FLAG;
-import static android.graphics.Paint.FILTER_BITMAP_FLAG;
-
-import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.PaintFlagsDrawFilter;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Process;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-/**
- * This class will be moved to androidx library. There shouldn't be any dependency outside
- * this package.
- */
-public class BaseIconFactory implements AutoCloseable {
-
-    private static final String TAG = "BaseIconFactory";
-    private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
-    static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
-    static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
-
-    private static final float ICON_BADGE_SCALE = 0.444f;
-
-    private final Rect mOldBounds = new Rect();
-    protected final Context mContext;
-    private final Canvas mCanvas;
-    private final PackageManager mPm;
-    private final ColorExtractor mColorExtractor;
-    private boolean mDisableColorExtractor;
-
-    protected final int mFillResIconDpi;
-    protected final int mIconBitmapSize;
-
-    private IconNormalizer mNormalizer;
-    private ShadowGenerator mShadowGenerator;
-    private final boolean mShapeDetection;
-
-    private Drawable mWrapperIcon;
-    private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
-
-    protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
-            boolean shapeDetection) {
-        mContext = context.getApplicationContext();
-        mShapeDetection = shapeDetection;
-        mFillResIconDpi = fillResIconDpi;
-        mIconBitmapSize = iconBitmapSize;
-
-        mPm = mContext.getPackageManager();
-        mColorExtractor = new ColorExtractor();
-
-        mCanvas = new Canvas();
-        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
-        clear();
-    }
-
-    protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
-        this(context, fillResIconDpi, iconBitmapSize, false);
-    }
-
-    protected void clear() {
-        mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
-        mDisableColorExtractor = false;
-    }
-
-    public ShadowGenerator getShadowGenerator() {
-        if (mShadowGenerator == null) {
-            mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
-        }
-        return mShadowGenerator;
-    }
-
-    public IconNormalizer getNormalizer() {
-        if (mNormalizer == null) {
-            mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
-        }
-        return mNormalizer;
-    }
-
-    @SuppressWarnings("deprecation")
-    public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
-        try {
-            Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
-            if (resources != null) {
-                final int id = resources.getIdentifier(iconRes.resourceName, null, null);
-                // do not stamp old legacy shortcuts as the app may have already forgotten about it
-                return createBadgedIconBitmap(
-                        resources.getDrawableForDensity(id, mFillResIconDpi),
-                        Process.myUserHandle() /* only available on primary user */,
-                        false /* do not apply legacy treatment */);
-            }
-        } catch (Exception e) {
-            // Icon not found.
-        }
-        return null;
-    }
-
-    public BitmapInfo createIconBitmap(Bitmap icon) {
-        if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
-            icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
-        }
-
-        return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
-    }
-
-    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
-            boolean shrinkNonAdaptiveIcons) {
-        return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
-    }
-
-    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
-            int iconAppTargetSdk) {
-        return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
-    }
-
-    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
-            int iconAppTargetSdk, boolean isInstantApp) {
-        return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
-    }
-
-    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
-            int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
-        boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
-                (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
-        return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
-    }
-
-    public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
-        boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
-                (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
-        return  createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
-    }
-
-    /**
-     * Creates bitmap using the source drawable and various parameters.
-     * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
-     *
-     * @param icon                      source of the icon
-     * @param user                      info can be used for a badge
-     * @param shrinkNonAdaptiveIcons    {@code true} if non adaptive icons should be treated
-     * @param isInstantApp              info can be used for a badge
-     * @param scale                     returns the scale result from normalization
-     * @return a bitmap suitable for disaplaying as an icon at various system UIs.
-     */
-    public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user,
-            boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
-        if (scale == null) {
-            scale = new float[1];
-        }
-        icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
-        Bitmap bitmap = createIconBitmap(icon, scale[0]);
-        if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
-            mCanvas.setBitmap(bitmap);
-            getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
-            mCanvas.setBitmap(null);
-        }
-
-        if (isInstantApp) {
-            badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
-        }
-        if (user != null) {
-            BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
-            Drawable badged = mPm.getUserBadgedIcon(drawable, user);
-            if (badged instanceof BitmapDrawable) {
-                bitmap = ((BitmapDrawable) badged).getBitmap();
-            } else {
-                bitmap = createIconBitmap(badged, 1f);
-            }
-        }
-        return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
-    }
-
-    public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
-        RectF iconBounds = new RectF();
-        float[] scale = new float[1];
-        icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
-        return createIconBitmap(icon,
-                Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
-    }
-
-    /**
-     * Sets the background color used for wrapped adaptive icon
-     */
-    public void setWrapperBackgroundColor(int color) {
-        mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
-    }
-
-    /**
-     * Disables the dominant color extraction for all icons loaded.
-     */
-    public void disableColorExtraction() {
-        mDisableColorExtractor = true;
-    }
-
-    private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
-            boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
-        if (icon == null) {
-            return null;
-        }
-        float scale = 1f;
-
-        if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
-            if (mWrapperIcon == null) {
-                mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
-                        .mutate();
-            }
-            AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
-            dr.setBounds(0, 0, 1, 1);
-            boolean[] outShape = new boolean[1];
-            scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
-            if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
-                FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
-                fsd.setDrawable(icon);
-                fsd.setScale(scale);
-                icon = dr;
-                scale = getNormalizer().getScale(icon, outIconBounds, null, null);
-
-                ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
-            }
-        } else {
-            scale = getNormalizer().getScale(icon, outIconBounds, null, null);
-        }
-
-        outScale[0] = scale;
-        return icon;
-    }
-
-    /**
-     * Adds the {@param badge} on top of {@param target} using the badge dimensions.
-     */
-    public void badgeWithDrawable(Bitmap target, Drawable badge) {
-        mCanvas.setBitmap(target);
-        badgeWithDrawable(mCanvas, badge);
-        mCanvas.setBitmap(null);
-    }
-
-    /**
-     * Adds the {@param badge} on top of {@param target} using the badge dimensions.
-     */
-    public void badgeWithDrawable(Canvas target, Drawable badge) {
-        int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize);
-        badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
-                mIconBitmapSize, mIconBitmapSize);
-        badge.draw(target);
-    }
-
-    private Bitmap createIconBitmap(Drawable icon, float scale) {
-        return createIconBitmap(icon, scale, mIconBitmapSize);
-    }
-
-    /**
-     * @param icon drawable that should be flattened to a bitmap
-     * @param scale the scale to apply before drawing {@param icon} on the canvas
-     */
-    public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
-        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
-        if (icon == null) {
-            return bitmap;
-        }
-        mCanvas.setBitmap(bitmap);
-        mOldBounds.set(icon.getBounds());
-
-        if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
-            int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
-                    Math.round(size * (1 - scale) / 2 ));
-            icon.setBounds(offset, offset, size - offset, size - offset);
-            icon.draw(mCanvas);
-        } else {
-            if (icon instanceof BitmapDrawable) {
-                BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
-                Bitmap b = bitmapDrawable.getBitmap();
-                if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
-                    bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
-                }
-            }
-            int width = size;
-            int height = size;
-
-            int intrinsicWidth = icon.getIntrinsicWidth();
-            int intrinsicHeight = icon.getIntrinsicHeight();
-            if (intrinsicWidth > 0 && intrinsicHeight > 0) {
-                // Scale the icon proportionally to the icon dimensions
-                final float ratio = (float) intrinsicWidth / intrinsicHeight;
-                if (intrinsicWidth > intrinsicHeight) {
-                    height = (int) (width / ratio);
-                } else if (intrinsicHeight > intrinsicWidth) {
-                    width = (int) (height * ratio);
-                }
-            }
-            final int left = (size - width) / 2;
-            final int top = (size - height) / 2;
-            icon.setBounds(left, top, left + width, top + height);
-            mCanvas.save();
-            mCanvas.scale(scale, scale, size / 2, size / 2);
-            icon.draw(mCanvas);
-            mCanvas.restore();
-
-        }
-        icon.setBounds(mOldBounds);
-        mCanvas.setBitmap(null);
-        return bitmap;
-    }
-
-    @Override
-    public void close() {
-        clear();
-    }
-
-    public BitmapInfo makeDefaultIcon(UserHandle user) {
-        return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
-                user, Build.VERSION.SDK_INT);
-    }
-
-    public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
-        return Resources.getSystem().getDrawableForDensity(
-                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
-                        ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
-                iconDpi);
-    }
-
-    /**
-     * Returns the correct badge size given an icon size
-     */
-    public static int getBadgeSizeForIconSize(int iconSize) {
-        return (int) (ICON_BADGE_SCALE * iconSize);
-    }
-
-    /**
-     * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
-     * This allows the badging to be done based on the action bitmap size rather than
-     * the scaled bitmap size.
-     */
-    private static class FixedSizeBitmapDrawable extends BitmapDrawable {
-
-        public FixedSizeBitmapDrawable(Bitmap bitmap) {
-            super(null, bitmap);
-        }
-
-        @Override
-        public int getIntrinsicHeight() {
-            return getBitmap().getWidth();
-        }
-
-        @Override
-        public int getIntrinsicWidth() {
-            return getBitmap().getWidth();
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
deleted file mode 100644
index 245561e..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ /dev/null
@@ -1,49 +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.icons;
-
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-
-public class BitmapInfo {
-
-    public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
-
-    public Bitmap icon;
-    public int color;
-
-    public void applyTo(BitmapInfo info) {
-        info.icon = icon;
-        info.color = color;
-    }
-
-    public final boolean isLowRes() {
-        return LOW_RES_ICON == icon;
-    }
-
-    public static BitmapInfo fromBitmap(Bitmap bitmap) {
-        return fromBitmap(bitmap, null);
-    }
-
-    public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) {
-        BitmapInfo info = new BitmapInfo();
-        info.icon = bitmap;
-        info.color = dominantColorExtractor != null
-                ? dominantColorExtractor.findDominantColorByHue(bitmap)
-                : 0;
-        return info;
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
deleted file mode 100644
index a66b929..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
+++ /dev/null
@@ -1,50 +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.icons;
-
-import android.annotation.TargetApi;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Picture;
-import android.os.Build;
-
-/**
- * Interface representing a bitmap draw operation.
- */
-public interface BitmapRenderer {
-
-    boolean USE_HARDWARE_BITMAP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
-
-    static Bitmap createSoftwareBitmap(int width, int height, BitmapRenderer renderer) {
-        Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        renderer.draw(new Canvas(result));
-        return result;
-    }
-
-    @TargetApi(Build.VERSION_CODES.P)
-    static Bitmap createHardwareBitmap(int width, int height, BitmapRenderer renderer) {
-        if (!USE_HARDWARE_BITMAP) {
-            return createSoftwareBitmap(width, height, renderer);
-        }
-
-        Picture picture = new Picture();
-        renderer.draw(picture.beginRecording(width, height));
-        picture.endRecording();
-        return Bitmap.createBitmap(picture);
-    }
-
-    void draw(Canvas out);
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
deleted file mode 100644
index 87bda82..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
+++ /dev/null
@@ -1,127 +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.icons;
-
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.util.SparseArray;
-import java.util.Arrays;
-
-/**
- * Utility class for extracting colors from a bitmap.
- */
-public class ColorExtractor {
-
-    private final int NUM_SAMPLES = 20;
-    private final float[] mTmpHsv = new float[3];
-    private final float[] mTmpHueScoreHistogram = new float[360];
-    private final int[] mTmpPixels = new int[NUM_SAMPLES];
-    private final SparseArray<Float> mTmpRgbScores = new SparseArray<>();
-
-    /**
-     * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
-     * @param bitmap The bitmap to scan
-     */
-    public int findDominantColorByHue(Bitmap bitmap) {
-        return findDominantColorByHue(bitmap, NUM_SAMPLES);
-    }
-
-    /**
-     * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
-     * @param bitmap The bitmap to scan
-     */
-    public int findDominantColorByHue(Bitmap bitmap, int samples) {
-        final int height = bitmap.getHeight();
-        final int width = bitmap.getWidth();
-        int sampleStride = (int) Math.sqrt((height * width) / samples);
-        if (sampleStride < 1) {
-            sampleStride = 1;
-        }
-
-        // This is an out-param, for getting the hsv values for an rgb
-        float[] hsv = mTmpHsv;
-        Arrays.fill(hsv, 0);
-
-        // First get the best hue, by creating a histogram over 360 hue buckets,
-        // where each pixel contributes a score weighted by saturation, value, and alpha.
-        float[] hueScoreHistogram = mTmpHueScoreHistogram;
-        Arrays.fill(hueScoreHistogram, 0);
-        float highScore = -1;
-        int bestHue = -1;
-
-        int[] pixels = mTmpPixels;
-        Arrays.fill(pixels, 0);
-        int pixelCount = 0;
-
-        for (int y = 0; y < height; y += sampleStride) {
-            for (int x = 0; x < width; x += sampleStride) {
-                int argb = bitmap.getPixel(x, y);
-                int alpha = 0xFF & (argb >> 24);
-                if (alpha < 0x80) {
-                    // Drop mostly-transparent pixels.
-                    continue;
-                }
-                // Remove the alpha channel.
-                int rgb = argb | 0xFF000000;
-                Color.colorToHSV(rgb, hsv);
-                // Bucket colors by the 360 integer hues.
-                int hue = (int) hsv[0];
-                if (hue < 0 || hue >= hueScoreHistogram.length) {
-                    // Defensively avoid array bounds violations.
-                    continue;
-                }
-                if (pixelCount < samples) {
-                    pixels[pixelCount++] = rgb;
-                }
-                float score = hsv[1] * hsv[2];
-                hueScoreHistogram[hue] += score;
-                if (hueScoreHistogram[hue] > highScore) {
-                    highScore = hueScoreHistogram[hue];
-                    bestHue = hue;
-                }
-            }
-        }
-
-        SparseArray<Float> rgbScores = mTmpRgbScores;
-        rgbScores.clear();
-        int bestColor = 0xff000000;
-        highScore = -1;
-        // Go back over the RGB colors that match the winning hue,
-        // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
-        // The highest-scoring RGB color wins.
-        for (int i = 0; i < pixelCount; i++) {
-            int rgb = pixels[i];
-            Color.colorToHSV(rgb, hsv);
-            int hue = (int) hsv[0];
-            if (hue == bestHue) {
-                float s = hsv[1];
-                float v = hsv[2];
-                int bucket = (int) (s * 100) + (int) (v * 10000);
-                // Score by cumulative saturation * value.
-                float score = s * v;
-                Float oldTotal = rgbScores.get(bucket);
-                float newTotal = oldTotal == null ? score : oldTotal + score;
-                rgbScores.put(bucket, newTotal);
-                if (newTotal > highScore) {
-                    highScore = newTotal;
-                    // All the colors in the winning bucket are very similar. Last in wins.
-                    bestColor = rgb;
-                }
-            }
-        }
-        return bestColor;
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
deleted file mode 100644
index 97a0fd3..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.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.icons;
-
-import static android.graphics.Paint.ANTI_ALIAS_FLAG;
-import static android.graphics.Paint.FILTER_BITMAP_FLAG;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PathMeasure;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.util.Log;
-import android.view.ViewDebug;
-
-/**
- * Used to draw a notification dot on top of an icon.
- */
-public class DotRenderer {
-
-    private static final String TAG = "DotRenderer";
-
-    // The dot size is defined as a percentage of the app icon size.
-    private static final float SIZE_PERCENTAGE = 0.228f;
-
-    private final float mCircleRadius;
-    private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
-
-    private final Bitmap mBackgroundWithShadow;
-    private final float mBitmapOffset;
-
-    // Stores the center x and y position as a percentage (0 to 1) of the icon size
-    private final float[] mRightDotPosition;
-    private final float[] mLeftDotPosition;
-
-    public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
-        int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
-        ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT);
-        builder.ambientShadowAlpha = 88;
-        mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);
-        mCircleRadius = builder.radius;
-
-        mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width.
-
-        // Find the points on the path that are closest to the top left and right corners.
-        mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1);
-        mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1);
-    }
-
-    private static float[] getPathPoint(Path path, float size, float direction) {
-        float halfSize = size / 2;
-        // Small delta so that we don't get a zero size triangle
-        float delta = 1;
-
-        float x = halfSize + direction * halfSize;
-        Path trianglePath = new Path();
-        trianglePath.moveTo(halfSize, halfSize);
-        trianglePath.lineTo(x + delta * direction, 0);
-        trianglePath.lineTo(x, -delta);
-        trianglePath.close();
-
-        trianglePath.op(path, Path.Op.INTERSECT);
-        float[] pos = new float[2];
-        new PathMeasure(trianglePath, false).getPosTan(0, pos, null);
-
-        pos[0] = pos[0] / size;
-        pos[1] = pos[1] / size;
-        return pos;
-    }
-
-    public float[] getLeftDotPosition() {
-        return mLeftDotPosition;
-    }
-
-    public float[] getRightDotPosition() {
-        return mRightDotPosition;
-    }
-
-    /**
-     * Draw a circle on top of the canvas according to the given params.
-     */
-    public void draw(Canvas canvas, DrawParams params) {
-        if (params == null) {
-            Log.e(TAG, "Invalid null argument(s) passed in call to draw.");
-            return;
-        }
-        canvas.save();
-
-        Rect iconBounds = params.iconBounds;
-        float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition;
-        float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0];
-        float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1];
-
-        // Ensure dot fits entirely in canvas clip bounds.
-        Rect canvasBounds = canvas.getClipBounds();
-        float offsetX = params.leftAlign
-                ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset))
-                : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset));
-        float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset));
-
-        // We draw the dot relative to its center.
-        canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY);
-        canvas.scale(params.scale, params.scale);
-
-        mCirclePaint.setColor(Color.BLACK);
-        canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);
-        mCirclePaint.setColor(params.color);
-        canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint);
-        canvas.restore();
-    }
-
-    public static class DrawParams {
-        /** The color (possibly based on the icon) to use for the dot. */
-        @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
-        public int color;
-        /** The bounds of the icon that the dot is drawn on top of. */
-        @ViewDebug.ExportedProperty(category = "notification dot")
-        public Rect iconBounds = new Rect();
-        /** The progress of the animation, from 0 to 1. */
-        @ViewDebug.ExportedProperty(category = "notification dot")
-        public float scale;
-        /** Whether the dot should align to the top left of the icon rather than the top right. */
-        @ViewDebug.ExportedProperty(category = "notification dot")
-        public boolean leftAlign;
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
deleted file mode 100644
index 516965e..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.android.launcher3.icons;
-
-import android.content.res.Resources;
-import android.content.res.Resources.Theme;
-import android.graphics.Canvas;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.DrawableWrapper;
-import android.util.AttributeSet;
-
-import org.xmlpull.v1.XmlPullParser;
-
-/**
- * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
- */
-public class FixedScaleDrawable extends DrawableWrapper {
-
-    // TODO b/33553066 use the constant defined in MaskableIconDrawable
-    private static final float LEGACY_ICON_SCALE = .7f * .6667f;
-    private float mScaleX, mScaleY;
-
-    public FixedScaleDrawable() {
-        super(new ColorDrawable());
-        mScaleX = LEGACY_ICON_SCALE;
-        mScaleY = LEGACY_ICON_SCALE;
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        int saveCount = canvas.save();
-        canvas.scale(mScaleX, mScaleY,
-                getBounds().exactCenterX(), getBounds().exactCenterY());
-        super.draw(canvas);
-        canvas.restoreToCount(saveCount);
-    }
-
-    @Override
-    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
-
-    @Override
-    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
-
-    public void setScale(float scale) {
-        float h = getIntrinsicHeight();
-        float w = getIntrinsicWidth();
-        mScaleX = scale * LEGACY_ICON_SCALE;
-        mScaleY = scale * LEGACY_ICON_SCALE;
-        if (h > w && w > 0) {
-            mScaleX *= w / h;
-        } else if (w > h && h > 0) {
-            mScaleY *= h / w;
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
deleted file mode 100644
index 3e818a5..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons;
-
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.RegionIterator;
-import android.util.Log;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-import androidx.annotation.ColorInt;
-
-public class GraphicsUtils {
-
-    private static final String TAG = "GraphicsUtils";
-
-    /**
-     * Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version,
-     * it bounds the alpha in valid range instead of throwing an exception to allow for safer
-     * interpolation of color animations
-     */
-    @ColorInt
-    public static int setColorAlphaBound(int color, int alpha) {
-        if (alpha < 0) {
-            alpha = 0;
-        } else if (alpha > 255) {
-            alpha = 255;
-        }
-        return (color & 0x00ffffff) | (alpha << 24);
-    }
-
-    /**
-     * Compresses the bitmap to a byte array for serialization.
-     */
-    public static byte[] flattenBitmap(Bitmap bitmap) {
-        // Try go guesstimate how much space the icon will take when serialized
-        // to avoid unnecessary allocations/copies during the write (4 bytes per pixel).
-        int size = bitmap.getWidth() * bitmap.getHeight() * 4;
-        ByteArrayOutputStream out = new ByteArrayOutputStream(size);
-        try {
-            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
-            out.flush();
-            out.close();
-            return out.toByteArray();
-        } catch (IOException e) {
-            Log.w(TAG, "Could not write bitmap");
-            return null;
-        }
-    }
-
-    public static int getArea(Region r) {
-        RegionIterator itr = new RegionIterator(r);
-        int area = 0;
-        Rect tempRect = new Rect();
-        while (itr.next(tempRect)) {
-            area += tempRect.width() * tempRect.height();
-        }
-        return area;
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
deleted file mode 100644
index de39e79..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
+++ /dev/null
@@ -1,411 +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.icons;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.util.Log;
-
-import java.nio.ByteBuffer;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class IconNormalizer {
-
-    private static final String TAG = "IconNormalizer";
-    private static final boolean DEBUG = false;
-    // Ratio of icon visible area to full icon size for a square shaped icon
-    private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
-    // Ratio of icon visible area to full icon size for a circular shaped icon
-    private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
-
-    private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
-
-    // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
-    private static final float LINEAR_SCALE_SLOPE =
-            (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
-
-    private static final int MIN_VISIBLE_ALPHA = 40;
-
-    // Shape detection related constants
-    private static final float BOUND_RATIO_MARGIN = .05f;
-    private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
-    private static final float SCALE_NOT_INITIALIZED = 0;
-
-    // Ratio of the diameter of an normalized circular icon to the actual icon size.
-    public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f;
-
-    private final int mMaxSize;
-    private final Bitmap mBitmap;
-    private final Canvas mCanvas;
-    private final Paint mPaintMaskShape;
-    private final Paint mPaintMaskShapeOutline;
-    private final byte[] mPixels;
-
-    private final RectF mAdaptiveIconBounds;
-    private float mAdaptiveIconScale;
-
-    private boolean mEnableShapeDetection;
-
-    // for each y, stores the position of the leftmost x and the rightmost x
-    private final float[] mLeftBorder;
-    private final float[] mRightBorder;
-    private final Rect mBounds;
-    private final Path mShapePath;
-    private final Matrix mMatrix;
-
-    /** package private **/
-    IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) {
-        // Use twice the icon size as maximum size to avoid scaling down twice.
-        mMaxSize = iconBitmapSize * 2;
-        mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
-        mCanvas = new Canvas(mBitmap);
-        mPixels = new byte[mMaxSize * mMaxSize];
-        mLeftBorder = new float[mMaxSize];
-        mRightBorder = new float[mMaxSize];
-        mBounds = new Rect();
-        mAdaptiveIconBounds = new RectF();
-
-        mPaintMaskShape = new Paint();
-        mPaintMaskShape.setColor(Color.RED);
-        mPaintMaskShape.setStyle(Paint.Style.FILL);
-        mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
-
-        mPaintMaskShapeOutline = new Paint();
-        mPaintMaskShapeOutline.setStrokeWidth(
-                2 * context.getResources().getDisplayMetrics().density);
-        mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
-        mPaintMaskShapeOutline.setColor(Color.BLACK);
-        mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-
-        mShapePath = new Path();
-        mMatrix = new Matrix();
-        mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
-        mEnableShapeDetection = shapeDetection;
-    }
-
-    private static float getScale(float hullArea, float boundingArea, float fullArea) {
-        float hullByRect = hullArea / boundingArea;
-        float scaleRequired;
-        if (hullByRect < CIRCLE_AREA_BY_RECT) {
-            scaleRequired = MAX_CIRCLE_AREA_FACTOR;
-        } else {
-            scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
-        }
-
-        float areaScale = hullArea / fullArea;
-        // Use sqrt of the final ratio as the images is scaled across both width and height.
-        return areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
-    }
-
-    /**
-     * @param d Should be AdaptiveIconDrawable
-     * @param size Canvas size to use
-     */
-    @TargetApi(Build.VERSION_CODES.O)
-    public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) {
-        Rect tmpBounds = new Rect(d.getBounds());
-        d.setBounds(0, 0, size, size);
-
-        Path path = ((AdaptiveIconDrawable) d).getIconMask();
-        Region region = new Region();
-        region.setPath(path, new Region(0, 0, size, size));
-
-        Rect hullBounds = region.getBounds();
-        int hullArea = GraphicsUtils.getArea(region);
-
-        if (outBounds != null) {
-            float sizeF = size;
-            outBounds.set(
-                    hullBounds.left / sizeF,
-                    hullBounds.top / sizeF,
-                    1 - (hullBounds.right / sizeF),
-                    1 - (hullBounds.bottom / sizeF));
-        }
-        d.setBounds(tmpBounds);
-        return getScale(hullArea, hullArea, size * size);
-    }
-
-    /**
-     * Returns if the shape of the icon is same as the path.
-     * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
-     */
-    private boolean isShape(Path maskPath) {
-        // Condition1:
-        // If width and height of the path not close to a square, then the icon shape is
-        // not same as the mask shape.
-        float iconRatio = ((float) mBounds.width()) / mBounds.height();
-        if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
-            if (DEBUG) {
-                Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
-            }
-            return false;
-        }
-
-        // Condition 2:
-        // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
-        // should generate transparent image, if the actual icon is equivalent to the shape.
-
-        // Fit the shape within the icon's bounding box
-        mMatrix.reset();
-        mMatrix.setScale(mBounds.width(), mBounds.height());
-        mMatrix.postTranslate(mBounds.left, mBounds.top);
-        maskPath.transform(mMatrix, mShapePath);
-
-        // XOR operation
-        mCanvas.drawPath(mShapePath, mPaintMaskShape);
-
-        // DST_OUT operation around the mask path outline
-        mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
-
-        // Check if the result is almost transparent
-        return isTransparentBitmap();
-    }
-
-    /**
-     * Used to determine if certain the bitmap is transparent.
-     */
-    private boolean isTransparentBitmap() {
-        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
-        buffer.rewind();
-        mBitmap.copyPixelsToBuffer(buffer);
-
-        int y = mBounds.top;
-        // buffer position
-        int index = y * mMaxSize;
-        // buffer shift after every row, width of buffer = mMaxSize
-        int rowSizeDiff = mMaxSize - mBounds.right;
-
-        int sum = 0;
-        for (; y < mBounds.bottom; y++) {
-            index += mBounds.left;
-            for (int x = mBounds.left; x < mBounds.right; x++) {
-                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
-                    sum++;
-                }
-                index++;
-            }
-            index += rowSizeDiff;
-        }
-
-        float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
-        return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
-    }
-
-    /**
-     * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
-     * matches the design guidelines for a launcher icon.
-     *
-     * We first calculate the convex hull of the visible portion of the icon.
-     * This hull then compared with the bounding rectangle of the hull to find how closely it
-     * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
-     * ideal solution but it gives satisfactory result without affecting the performance.
-     *
-     * This closeness is used to determine the ratio of hull area to the full icon size.
-     * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
-     *
-     * @param outBounds optional rect to receive the fraction distance from each edge.
-     */
-    public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
-            @Nullable Path path, @Nullable boolean[] outMaskShape) {
-        if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
-            if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
-                mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds);
-            }
-            if (outBounds != null) {
-                outBounds.set(mAdaptiveIconBounds);
-            }
-            return mAdaptiveIconScale;
-        }
-        int width = d.getIntrinsicWidth();
-        int height = d.getIntrinsicHeight();
-        if (width <= 0 || height <= 0) {
-            width = width <= 0 || width > mMaxSize ? mMaxSize : width;
-            height = height <= 0 || height > mMaxSize ? mMaxSize : height;
-        } else if (width > mMaxSize || height > mMaxSize) {
-            int max = Math.max(width, height);
-            width = mMaxSize * width / max;
-            height = mMaxSize * height / max;
-        }
-
-        mBitmap.eraseColor(Color.TRANSPARENT);
-        d.setBounds(0, 0, width, height);
-        d.draw(mCanvas);
-
-        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
-        buffer.rewind();
-        mBitmap.copyPixelsToBuffer(buffer);
-
-        // Overall bounds of the visible icon.
-        int topY = -1;
-        int bottomY = -1;
-        int leftX = mMaxSize + 1;
-        int rightX = -1;
-
-        // Create border by going through all pixels one row at a time and for each row find
-        // the first and the last non-transparent pixel. Set those values to mLeftBorder and
-        // mRightBorder and use -1 if there are no visible pixel in the row.
-
-        // buffer position
-        int index = 0;
-        // buffer shift after every row, width of buffer = mMaxSize
-        int rowSizeDiff = mMaxSize - width;
-        // first and last position for any row.
-        int firstX, lastX;
-
-        for (int y = 0; y < height; y++) {
-            firstX = lastX = -1;
-            for (int x = 0; x < width; x++) {
-                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
-                    if (firstX == -1) {
-                        firstX = x;
-                    }
-                    lastX = x;
-                }
-                index++;
-            }
-            index += rowSizeDiff;
-
-            mLeftBorder[y] = firstX;
-            mRightBorder[y] = lastX;
-
-            // If there is at least one visible pixel, update the overall bounds.
-            if (firstX != -1) {
-                bottomY = y;
-                if (topY == -1) {
-                    topY = y;
-                }
-
-                leftX = Math.min(leftX, firstX);
-                rightX = Math.max(rightX, lastX);
-            }
-        }
-
-        if (topY == -1 || rightX == -1) {
-            // No valid pixels found. Do not scale.
-            return 1;
-        }
-
-        convertToConvexArray(mLeftBorder, 1, topY, bottomY);
-        convertToConvexArray(mRightBorder, -1, topY, bottomY);
-
-        // Area of the convex hull
-        float area = 0;
-        for (int y = 0; y < height; y++) {
-            if (mLeftBorder[y] <= -1) {
-                continue;
-            }
-            area += mRightBorder[y] - mLeftBorder[y] + 1;
-        }
-
-        mBounds.left = leftX;
-        mBounds.right = rightX;
-
-        mBounds.top = topY;
-        mBounds.bottom = bottomY;
-
-        if (outBounds != null) {
-            outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
-                    1 - ((float) mBounds.right) / width,
-                    1 - ((float) mBounds.bottom) / height);
-        }
-        if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) {
-            outMaskShape[0] = isShape(path);
-        }
-        // Area of the rectangle required to fit the convex hull
-        float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
-        return getScale(area, rectArea, width * height);
-    }
-
-    /**
-     * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
-     * (except on either ends) with appropriate values.
-     * @param xCoordinates map of x coordinate per y.
-     * @param direction 1 for left border and -1 for right border.
-     * @param topY the first Y position (inclusive) with a valid value.
-     * @param bottomY the last Y position (inclusive) with a valid value.
-     */
-    private static void convertToConvexArray(
-            float[] xCoordinates, int direction, int topY, int bottomY) {
-        int total = xCoordinates.length;
-        // The tangent at each pixel.
-        float[] angles = new float[total - 1];
-
-        int first = topY; // First valid y coordinate
-        int last = -1;    // Last valid y coordinate which didn't have a missing value
-
-        float lastAngle = Float.MAX_VALUE;
-
-        for (int i = topY + 1; i <= bottomY; i++) {
-            if (xCoordinates[i] <= -1) {
-                continue;
-            }
-            int start;
-
-            if (lastAngle == Float.MAX_VALUE) {
-                start = first;
-            } else {
-                float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
-                start = last;
-                // If this position creates a concave angle, keep moving up until we find a
-                // position which creates a convex angle.
-                if ((currentAngle - lastAngle) * direction < 0) {
-                    while (start > first) {
-                        start --;
-                        currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
-                        if ((currentAngle - angles[start]) * direction >= 0) {
-                            break;
-                        }
-                    }
-                }
-            }
-
-            // Reset from last check
-            lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
-            // Update all the points from start.
-            for (int j = start; j < i; j++) {
-                angles[j] = lastAngle;
-                xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
-            }
-            last = i;
-        }
-    }
-
-    /**
-     * @return The diameter of the normalized circle that fits inside of the square (size x size).
-     */
-    public static int getNormalizedCircleSize(int size) {
-        float area = size * size * MAX_CIRCLE_AREA_FACTOR;
-        return (int) Math.round(Math.sqrt((4 * area) / Math.PI));
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
deleted file mode 100644
index 5df8043..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
+++ /dev/null
@@ -1,170 +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.icons;
-
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.BlurMaskFilter;
-import android.graphics.BlurMaskFilter.Blur;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.RectF;
-
-/**
- * Utility class to add shadows to bitmaps.
- */
-public class ShadowGenerator {
-    public static final float BLUR_FACTOR = 0.5f/48;
-
-    // Percent of actual icon size
-    public static final float KEY_SHADOW_DISTANCE = 1f/48;
-    private static final int KEY_SHADOW_ALPHA = 61;
-    // Percent of actual icon size
-    private static final float HALF_DISTANCE = 0.5f;
-    private static final int AMBIENT_SHADOW_ALPHA = 30;
-
-    private final int mIconSize;
-
-    private final Paint mBlurPaint;
-    private final Paint mDrawPaint;
-    private final BlurMaskFilter mDefaultBlurMaskFilter;
-
-    public ShadowGenerator(int iconSize) {
-        mIconSize = iconSize;
-        mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-        mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-        mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
-    }
-
-    public synchronized void recreateIcon(Bitmap icon, Canvas out) {
-        recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
-    }
-
-    public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
-            int ambientAlpha, int keyAlpha, Canvas out) {
-        int[] offset = new int[2];
-        mBlurPaint.setMaskFilter(blurMaskFilter);
-        Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
-
-        // Draw ambient shadow
-        mDrawPaint.setAlpha(ambientAlpha);
-        out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
-
-        // Draw key shadow
-        mDrawPaint.setAlpha(keyAlpha);
-        out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, mDrawPaint);
-
-        // Draw the icon
-        mDrawPaint.setAlpha(255);
-        out.drawBitmap(icon, 0, 0, mDrawPaint);
-    }
-
-    /**
-     * Returns the minimum amount by which an icon with {@param bounds} should be scaled
-     * so that the shadows do not get clipped.
-     */
-    public static float getScaleForBounds(RectF bounds) {
-        float scale = 1;
-
-        // For top, left & right, we need same space.
-        float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top);
-        if (minSide < BLUR_FACTOR) {
-            scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide);
-        }
-
-        float bottomSpace = BLUR_FACTOR + KEY_SHADOW_DISTANCE;
-        if (bounds.bottom < bottomSpace) {
-            scale = Math.min(scale, (HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom));
-        }
-        return scale;
-    }
-
-    public static class Builder {
-
-        public final RectF bounds = new RectF();
-        public final int color;
-
-        public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA;
-
-        public float shadowBlur;
-
-        public float keyShadowDistance;
-        public int keyShadowAlpha = KEY_SHADOW_ALPHA;
-        public float radius;
-
-        public Builder(int color) {
-            this.color = color;
-        }
-
-        public Builder setupBlurForSize(int height) {
-            shadowBlur = height * 1f / 24;
-            keyShadowDistance = height * 1f / 16;
-            return this;
-        }
-
-        public Bitmap createPill(int width, int height) {
-            return createPill(width, height, height / 2f);
-        }
-
-        public Bitmap createPill(int width, int height, float r) {
-            radius = r;
-
-            int centerX = Math.round(width / 2f + shadowBlur);
-            int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
-            int center = Math.max(centerX, centerY);
-            bounds.set(0, 0, width, height);
-            bounds.offsetTo(center - width / 2f, center - height / 2f);
-
-            int size = center * 2;
-            Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888);
-            drawShadow(new Canvas(result));
-            return result;
-        }
-
-        public void drawShadow(Canvas c) {
-            Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-            p.setColor(color);
-
-            // Key shadow
-            p.setShadowLayer(shadowBlur, 0, keyShadowDistance,
-                    setColorAlphaBound(Color.BLACK, keyShadowAlpha));
-            c.drawRoundRect(bounds, radius, radius, p);
-
-            // Ambient shadow
-            p.setShadowLayer(shadowBlur, 0, 0,
-                    setColorAlphaBound(Color.BLACK, ambientShadowAlpha));
-            c.drawRoundRect(bounds, radius, radius, p);
-
-            if (Color.alpha(color) < 255) {
-                // Clear any content inside the pill-rect for translucent fill.
-                p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-                p.clearShadowLayer();
-                p.setColor(Color.BLACK);
-                c.drawRoundRect(bounds, radius, radius, p);
-
-                p.setXfermode(null);
-                p.setColor(color);
-                c.drawRoundRect(bounds, radius, radius, p);
-            }
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
deleted file mode 100644
index 5cde547..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ /dev/null
@@ -1,576 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons.cache;
-
-import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
-import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-
-import android.content.ComponentName;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.LocaleList;
-import android.os.Looper;
-import android.os.Process;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.BitmapRenderer;
-import com.android.launcher3.icons.GraphicsUtils;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SQLiteCacheHelper;
-
-import java.util.AbstractMap;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Supplier;
-
-public abstract class BaseIconCache {
-
-    private static final String TAG = "BaseIconCache";
-    private static final boolean DEBUG = false;
-
-    private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
-
-    // Empty class name is used for storing package default entry.
-    public static final String EMPTY_CLASS_NAME = ".";
-
-    public static class CacheEntry extends BitmapInfo {
-        public CharSequence title = "";
-        public CharSequence contentDescription = "";
-    }
-
-    private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
-
-    protected final Context mContext;
-    protected final PackageManager mPackageManager;
-
-    private final Map<ComponentKey, CacheEntry> mCache;
-    protected final Handler mWorkerHandler;
-
-    protected int mIconDpi;
-    protected IconDB mIconDb;
-    protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
-    protected String mSystemState = "";
-
-    private final String mDbFileName;
-    private final BitmapFactory.Options mDecodeOptions;
-    private final Looper mBgLooper;
-
-    public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
-            int iconDpi, int iconPixelSize, boolean inMemoryCache) {
-        mContext = context;
-        mDbFileName = dbFileName;
-        mPackageManager = context.getPackageManager();
-        mBgLooper = bgLooper;
-        mWorkerHandler = new Handler(mBgLooper);
-
-        if (inMemoryCache) {
-            mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
-        } else {
-            // Use a placeholder cache
-            mCache = new AbstractMap<ComponentKey, CacheEntry>() {
-                @Override
-                public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
-                    return Collections.emptySet();
-                }
-
-                @Override
-                public CacheEntry put(ComponentKey key, CacheEntry value) {
-                    return value;
-                }
-            };
-        }
-
-        if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            mDecodeOptions = new BitmapFactory.Options();
-            mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
-        } else {
-            mDecodeOptions = null;
-        }
-
-        updateSystemState();
-        mIconDpi = iconDpi;
-        mIconDb = new IconDB(context, dbFileName, iconPixelSize);
-    }
-
-    /**
-     * Returns the persistable serial number for {@param user}. Subclass should implement proper
-     * caching strategy to avoid making binder call every time.
-     */
-    protected abstract long getSerialNumberForUser(UserHandle user);
-
-    /**
-     * Return true if the given app is an instant app and should be badged appropriately.
-     */
-    protected abstract boolean isInstantApp(ApplicationInfo info);
-
-    /**
-     * Opens and returns an icon factory. The factory is recycled by the caller.
-     */
-    protected abstract BaseIconFactory getIconFactory();
-
-    public void updateIconParams(int iconDpi, int iconPixelSize) {
-        mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
-    }
-
-    private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
-        mIconDpi = iconDpi;
-        mDefaultIcons.clear();
-        mIconDb.clear();
-        mIconDb.close();
-        mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
-        mCache.clear();
-    }
-
-    private Drawable getFullResIcon(Resources resources, int iconId) {
-        if (resources != null && iconId != 0) {
-            try {
-                return resources.getDrawableForDensity(iconId, mIconDpi);
-            } catch (Resources.NotFoundException e) { }
-        }
-        return getFullResDefaultActivityIcon(mIconDpi);
-    }
-
-    public Drawable getFullResIcon(String packageName, int iconId) {
-        try {
-            return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
-        } catch (PackageManager.NameNotFoundException e) { }
-        return getFullResDefaultActivityIcon(mIconDpi);
-    }
-
-    public Drawable getFullResIcon(ActivityInfo info) {
-        try {
-            return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
-                    info.getIconResource());
-        } catch (PackageManager.NameNotFoundException e) { }
-        return getFullResDefaultActivityIcon(mIconDpi);
-    }
-
-    private BitmapInfo makeDefaultIcon(UserHandle user) {
-        try (BaseIconFactory li = getIconFactory()) {
-            return li.makeDefaultIcon(user);
-        }
-    }
-
-    /**
-     * Remove any records for the supplied ComponentName.
-     */
-    public synchronized void remove(ComponentName componentName, UserHandle user) {
-        mCache.remove(new ComponentKey(componentName, user));
-    }
-
-    /**
-     * Remove any records for the supplied package name from memory.
-     */
-    private void removeFromMemCacheLocked(String packageName, UserHandle user) {
-        HashSet<ComponentKey> forDeletion = new HashSet<>();
-        for (ComponentKey key: mCache.keySet()) {
-            if (key.componentName.getPackageName().equals(packageName)
-                    && key.user.equals(user)) {
-                forDeletion.add(key);
-            }
-        }
-        for (ComponentKey condemned: forDeletion) {
-            mCache.remove(condemned);
-        }
-    }
-
-    /**
-     * Removes the entries related to the given package in memory and persistent DB.
-     */
-    public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
-        removeFromMemCacheLocked(packageName, user);
-        long userSerial = getSerialNumberForUser(user);
-        mIconDb.delete(
-                IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
-                new String[]{packageName + "/%", Long.toString(userSerial)});
-    }
-
-    public IconCacheUpdateHandler getUpdateHandler() {
-        updateSystemState();
-        return new IconCacheUpdateHandler(this);
-    }
-
-    /**
-     * Refreshes the system state definition used to check the validity of the cache. It
-     * incorporates all the properties that can affect the cache like the list of enabled locale
-     * and system-version.
-     */
-    private void updateSystemState() {
-        mLocaleList = mContext.getResources().getConfiguration().getLocales();
-        mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
-    }
-
-    protected String getIconSystemState(String packageName) {
-        return mSystemState;
-    }
-
-    /**
-     * Adds an entry into the DB and the in-memory cache.
-     * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
-     *                        the memory. This is useful then the previous bitmap was created using
-     *                        old data.
-     * package private
-     */
-    protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
-            PackageInfo info, long userSerial, boolean replaceExisting) {
-        UserHandle user = cachingLogic.getUser(object);
-        ComponentName componentName = cachingLogic.getComponent(object);
-
-        final ComponentKey key = new ComponentKey(componentName, user);
-        CacheEntry entry = null;
-        if (!replaceExisting) {
-            entry = mCache.get(key);
-            // We can't reuse the entry if the high-res icon is not present.
-            if (entry == null || entry.icon == null || entry.isLowRes()) {
-                entry = null;
-            }
-        }
-        if (entry == null) {
-            entry = new CacheEntry();
-            cachingLogic.loadIcon(mContext, object, entry);
-        }
-        // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
-        // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
-        // an empty entry.
-        if (entry.icon == null) return;
-        entry.title = cachingLogic.getLabel(object);
-        entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-        if (cachingLogic.addToMemCache()) mCache.put(key, entry);
-
-        ContentValues values = newContentValues(entry, entry.title.toString(),
-                componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
-        addIconToDB(values, componentName, info, userSerial);
-    }
-
-    /**
-     * Updates {@param values} to contain versioning information and adds it to the DB.
-     * @param values {@link ContentValues} containing icon & title
-     */
-    private void addIconToDB(ContentValues values, ComponentName key,
-            PackageInfo info, long userSerial) {
-        values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
-        values.put(IconDB.COLUMN_USER, userSerial);
-        values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
-        values.put(IconDB.COLUMN_VERSION, info.versionCode);
-        mIconDb.insertOrReplace(values);
-    }
-
-    public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
-        if (!mDefaultIcons.containsKey(user)) {
-            mDefaultIcons.put(user, makeDefaultIcon(user));
-        }
-        return mDefaultIcons.get(user);
-    }
-
-    public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
-        return getDefaultIcon(user).icon == icon;
-    }
-
-    /**
-     * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
-     * This method is not thread safe, it must be called from a synchronized method.
-     */
-    protected <T> CacheEntry cacheLocked(
-            @NonNull ComponentName componentName, @NonNull UserHandle user,
-            @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
-            boolean usePackageIcon, boolean useLowResIcon) {
-        assertWorkerThread();
-        ComponentKey cacheKey = new ComponentKey(componentName, user);
-        CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
-            entry = new CacheEntry();
-            if (cachingLogic.addToMemCache()) {
-                mCache.put(cacheKey, entry);
-            }
-
-            // Check the DB first.
-            T object = null;
-            boolean providerFetchedOnce = false;
-
-            if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
-                object = infoProvider.get();
-                providerFetchedOnce = true;
-
-                if (object != null) {
-                    cachingLogic.loadIcon(mContext, object, entry);
-                } else {
-                    if (usePackageIcon) {
-                        CacheEntry packageEntry = getEntryForPackageLocked(
-                                componentName.getPackageName(), user, false);
-                        if (packageEntry != null) {
-                            if (DEBUG) Log.d(TAG, "using package default icon for " +
-                                    componentName.toShortString());
-                            packageEntry.applyTo(entry);
-                            entry.title = packageEntry.title;
-                            entry.contentDescription = packageEntry.contentDescription;
-                        }
-                    }
-                    if (entry.icon == null) {
-                        if (DEBUG) Log.d(TAG, "using default icon for " +
-                                componentName.toShortString());
-                        getDefaultIcon(user).applyTo(entry);
-                    }
-                }
-            }
-
-            if (TextUtils.isEmpty(entry.title)) {
-                if (object == null && !providerFetchedOnce) {
-                    object = infoProvider.get();
-                    providerFetchedOnce = true;
-                }
-                if (object != null) {
-                    entry.title = cachingLogic.getLabel(object);
-                    entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-                }
-            }
-        }
-        return entry;
-    }
-
-    public synchronized void clear() {
-        assertWorkerThread();
-        mIconDb.clear();
-    }
-
-    /**
-     * Adds a default package entry in the cache. This entry is not persisted and will be removed
-     * when the cache is flushed.
-     */
-    public synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
-            Bitmap icon, CharSequence title) {
-        removeFromMemCacheLocked(packageName, user);
-
-        ComponentKey cacheKey = getPackageKey(packageName, user);
-        CacheEntry entry = mCache.get(cacheKey);
-
-        // For icon caching, do not go through DB. Just update the in-memory entry.
-        if (entry == null) {
-            entry = new CacheEntry();
-        }
-        if (!TextUtils.isEmpty(title)) {
-            entry.title = title;
-        }
-        if (icon != null) {
-            BaseIconFactory li = getIconFactory();
-            li.createIconBitmap(icon).applyTo(entry);
-            li.close();
-        }
-        if (!TextUtils.isEmpty(title) && entry.icon != null) {
-            mCache.put(cacheKey, entry);
-        }
-    }
-
-    private static ComponentKey getPackageKey(String packageName, UserHandle user) {
-        ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
-        return new ComponentKey(cn, user);
-    }
-
-    /**
-     * Gets an entry for the package, which can be used as a fallback entry for various components.
-     * This method is not thread safe, it must be called from a synchronized method.
-     */
-    protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
-            boolean useLowResIcon) {
-        assertWorkerThread();
-        ComponentKey cacheKey = getPackageKey(packageName, user);
-        CacheEntry entry = mCache.get(cacheKey);
-
-        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
-            entry = new CacheEntry();
-            boolean entryUpdated = true;
-
-            // Check the DB first.
-            if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
-                try {
-                    int flags = Process.myUserHandle().equals(user) ? 0 :
-                            PackageManager.GET_UNINSTALLED_PACKAGES;
-                    PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
-                    ApplicationInfo appInfo = info.applicationInfo;
-                    if (appInfo == null) {
-                        throw new NameNotFoundException("ApplicationInfo is null");
-                    }
-
-                    BaseIconFactory li = getIconFactory();
-                    // Load the full res icon for the application, but if useLowResIcon is set, then
-                    // only keep the low resolution icon instead of the larger full-sized icon
-                    BitmapInfo iconInfo = li.createBadgedIconBitmap(
-                            appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
-                            isInstantApp(appInfo));
-                    li.close();
-
-                    entry.title = appInfo.loadLabel(mPackageManager);
-                    entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-                    entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
-                    entry.color = iconInfo.color;
-
-                    // Add the icon in the DB here, since these do not get written during
-                    // package updates.
-                    ContentValues values = newContentValues(
-                            iconInfo, entry.title.toString(), packageName, null);
-                    addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user));
-
-                } catch (NameNotFoundException e) {
-                    if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
-                    entryUpdated = false;
-                }
-            }
-
-            // Only add a filled-out entry to the cache
-            if (entryUpdated) {
-                mCache.put(cacheKey, entry);
-            }
-        }
-        return entry;
-    }
-
-    private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
-        Cursor c = null;
-        try {
-            c = mIconDb.query(
-                    lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
-                    IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
-                    new String[]{
-                            cacheKey.componentName.flattenToString(),
-                            Long.toString(getSerialNumberForUser(cacheKey.user))});
-            if (c.moveToNext()) {
-                // Set the alpha to be 255, so that we never have a wrong color
-                entry.color = setColorAlphaBound(c.getInt(0), 255);
-                entry.title = c.getString(1);
-                if (entry.title == null) {
-                    entry.title = "";
-                    entry.contentDescription = "";
-                } else {
-                    entry.contentDescription = mPackageManager.getUserBadgedLabel(
-                            entry.title, cacheKey.user);
-                }
-
-                if (lowRes) {
-                    entry.icon = LOW_RES_ICON;
-                } else {
-                    byte[] data = c.getBlob(2);
-                    try {
-                        entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
-                                mDecodeOptions);
-                    } catch (Exception e) { }
-                }
-                return true;
-            }
-        } catch (SQLiteException e) {
-            Log.d(TAG, "Error reading icon cache", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Returns a cursor for an arbitrary query to the cache db
-     */
-    public synchronized Cursor queryCacheDb(String[] columns, String selection,
-            String[] selectionArgs) {
-        return mIconDb.query(columns, selection, selectionArgs);
-    }
-
-    /**
-     * Cache class to store the actual entries on disk
-     */
-    public static final class IconDB extends SQLiteCacheHelper {
-        private static final int RELEASE_VERSION = 27;
-
-        public static final String TABLE_NAME = "icons";
-        public static final String COLUMN_ROWID = "rowid";
-        public static final String COLUMN_COMPONENT = "componentName";
-        public static final String COLUMN_USER = "profileId";
-        public static final String COLUMN_LAST_UPDATED = "lastUpdated";
-        public static final String COLUMN_VERSION = "version";
-        public static final String COLUMN_ICON = "icon";
-        public static final String COLUMN_ICON_COLOR = "icon_color";
-        public static final String COLUMN_LABEL = "label";
-        public static final String COLUMN_SYSTEM_STATE = "system_state";
-        public static final String COLUMN_KEYWORDS = "keywords";
-
-        public static final String[] COLUMNS_HIGH_RES = new String[] {
-                IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
-        public static final String[] COLUMNS_LOW_RES = new String[] {
-                IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
-
-        public IconDB(Context context, String dbFileName, int iconPixelSize) {
-            super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
-        }
-
-        @Override
-        protected void onCreateTable(SQLiteDatabase db) {
-            db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
-                    + COLUMN_COMPONENT + " TEXT NOT NULL, "
-                    + COLUMN_USER + " INTEGER NOT NULL, "
-                    + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, "
-                    + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, "
-                    + COLUMN_ICON + " BLOB, "
-                    + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
-                    + COLUMN_LABEL + " TEXT, "
-                    + COLUMN_SYSTEM_STATE + " TEXT, "
-                    + COLUMN_KEYWORDS + " TEXT, "
-                    + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
-                    + ");");
-        }
-    }
-
-    private ContentValues newContentValues(BitmapInfo bitmapInfo, String label,
-            String packageName, @Nullable String keywords) {
-        ContentValues values = new ContentValues();
-        values.put(IconDB.COLUMN_ICON,
-                bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
-        values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
-
-        values.put(IconDB.COLUMN_LABEL, label);
-        values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
-        values.put(IconDB.COLUMN_KEYWORDS, keywords);
-        return values;
-    }
-
-    private void assertWorkerThread() {
-        if (Looper.myLooper() != mBgLooper) {
-            throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
deleted file mode 100644
index e40a9c2..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons.cache;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.LocaleList;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.icons.BitmapInfo;
-
-public interface CachingLogic<T> {
-
-    ComponentName getComponent(T object);
-
-    UserHandle getUser(T object);
-
-    CharSequence getLabel(T object);
-
-    void loadIcon(Context context, T object, BitmapInfo target);
-
-    /**
-     * Provides a option list of keywords to associate with this object
-     */
-    @Nullable
-    default String getKeywords(T object, LocaleList localeList) {
-        return null;
-    }
-
-    /**
-     * Returns true the object should be added to mem cache; otherwise returns false.
-     */
-    default boolean addToMemCache() {
-        return true;
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
deleted file mode 100644
index ee52934..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons.cache;
-
-import android.os.Handler;
-
-/**
- * A runnable that can be posted to a {@link Handler} which can be canceled.
- */
-public abstract class HandlerRunnable implements Runnable {
-
-    private final Handler mHandler;
-    private final Runnable mEndRunnable;
-
-    private boolean mEnded = false;
-    private boolean mCanceled = false;
-
-    public HandlerRunnable(Handler handler, Runnable endRunnable) {
-        mHandler = handler;
-        mEndRunnable = endRunnable;
-    }
-
-    /**
-     * Cancels this runnable from being run, only if it has not already run.
-     */
-    public void cancel() {
-        mHandler.removeCallbacks(this);
-        // TODO: This can actually cause onEnd to be called twice if the handler is already running
-        //       this runnable
-        // NOTE: This is currently run on whichever thread the caller is run on.
-        mCanceled = true;
-        onEnd();
-    }
-
-    /**
-     * @return whether this runnable was canceled.
-     */
-    protected boolean isCanceled() {
-        return mCanceled;
-    }
-
-    /**
-     * To be called by the implemention of this runnable. The end callback is done on whichever
-     * thread the caller is calling from.
-     */
-    public void onEnd() {
-        if (!mEnded) {
-            mEnded = true;
-            if (mEndRunnable != null) {
-                mEndRunnable.run();
-            }
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
deleted file mode 100644
index 8224966..0000000
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons.cache;
-
-import android.content.ComponentName;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.SparseBooleanArray;
-
-import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.Stack;
-
-/**
- * Utility class to handle updating the Icon cache
- */
-public class IconCacheUpdateHandler {
-
-    private static final String TAG = "IconCacheUpdateHandler";
-
-    /**
-     * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
-     * This mode is used for the first run.
-     */
-    private static final boolean MODE_SET_INVALID_ITEMS = true;
-
-    /**
-     * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
-     * subsequent runs, which essentially acts as set-union of all valid items.
-     */
-    private static final boolean MODE_CLEAR_VALID_ITEMS = false;
-
-    private static final Object ICON_UPDATE_TOKEN = new Object();
-
-    private final HashMap<String, PackageInfo> mPkgInfoMap;
-    private final BaseIconCache mIconCache;
-
-    private final HashMap<UserHandle, Set<String>> mPackagesToIgnore = new HashMap<>();
-
-    private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
-    private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
-
-    IconCacheUpdateHandler(BaseIconCache cache) {
-        mIconCache = cache;
-
-        mPkgInfoMap = new HashMap<>();
-
-        // Remove all active icon update tasks.
-        mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
-
-        createPackageInfoMap();
-    }
-
-    public void setPackagesToIgnore(UserHandle userHandle, Set<String> packages) {
-        mPackagesToIgnore.put(userHandle, packages);
-    }
-
-    private void createPackageInfoMap() {
-        PackageManager pm = mIconCache.mPackageManager;
-        for (PackageInfo info :
-                pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
-            mPkgInfoMap.put(info.packageName, info);
-        }
-    }
-
-    /**
-     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
-     * the DB and are updated.
-     * @return The set of packages for which icons have updated.
-     */
-    public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
-            OnUpdateCallback onUpdateCallback) {
-        // Filter the list per user
-        HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
-        int count = apps.size();
-        for (int i = 0; i < count; i++) {
-            T app = apps.get(i);
-            UserHandle userHandle = cachingLogic.getUser(app);
-            HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
-            if (componentMap == null) {
-                componentMap = new HashMap<>();
-                userComponentMap.put(userHandle, componentMap);
-            }
-            componentMap.put(cachingLogic.getComponent(app), app);
-        }
-
-        for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
-            updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
-        }
-
-        // From now on, clear every valid item from the global valid map.
-        mFilterMode = MODE_CLEAR_VALID_ITEMS;
-    }
-
-    /**
-     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
-     * the DB and are updated.
-     * @return The set of packages for which icons have updated.
-     */
-    @SuppressWarnings("unchecked")
-    private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
-            CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
-        Set<String> ignorePackages = mPackagesToIgnore.get(user);
-        if (ignorePackages == null) {
-            ignorePackages = Collections.emptySet();
-        }
-        long userSerial = mIconCache.getSerialNumberForUser(user);
-
-        Stack<T> appsToUpdate = new Stack<>();
-
-        try (Cursor c = mIconCache.mIconDb.query(
-                new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
-                        IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
-                        IconDB.COLUMN_SYSTEM_STATE},
-                IconDB.COLUMN_USER + " = ? ",
-                new String[]{Long.toString(userSerial)})) {
-
-            final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
-            final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
-            final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
-            final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
-            final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
-
-            while (c.moveToNext()) {
-                String cn = c.getString(indexComponent);
-                ComponentName component = ComponentName.unflattenFromString(cn);
-                PackageInfo info = mPkgInfoMap.get(component.getPackageName());
-
-                int rowId = c.getInt(rowIndex);
-                if (info == null) {
-                    if (!ignorePackages.contains(component.getPackageName())) {
-
-                        if (mFilterMode == MODE_SET_INVALID_ITEMS) {
-                            mIconCache.remove(component, user);
-                            mItemsToDelete.put(rowId, true);
-                        }
-                    }
-                    continue;
-                }
-                if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
-                    // Application is not present
-                    continue;
-                }
-
-                long updateTime = c.getLong(indexLastUpdate);
-                int version = c.getInt(indexVersion);
-                T app = componentMap.remove(component);
-                if (version == info.versionCode && updateTime == info.lastUpdateTime
-                        && TextUtils.equals(c.getString(systemStateIndex),
-                                mIconCache.getIconSystemState(info.packageName))) {
-
-                    if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
-                        mItemsToDelete.put(rowId, false);
-                    }
-                    continue;
-                }
-                if (app == null) {
-                    if (mFilterMode == MODE_SET_INVALID_ITEMS) {
-                        mIconCache.remove(component, user);
-                        mItemsToDelete.put(rowId, true);
-                    }
-                } else {
-                    appsToUpdate.add(app);
-                }
-            }
-        } catch (SQLiteException e) {
-            Log.d(TAG, "Error reading icon cache", e);
-            // Continue updating whatever we have read so far
-        }
-
-        // Insert remaining apps.
-        if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
-            Stack<T> appsToAdd = new Stack<>();
-            appsToAdd.addAll(componentMap.values());
-            new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
-                    onUpdateCallback).scheduleNext();
-        }
-    }
-
-    /**
-     * Commits all updates as part of the update handler to disk. Not more calls should be made
-     * to this class after this.
-     */
-    public void finish() {
-        // Commit all deletes
-        int deleteCount = 0;
-        StringBuilder queryBuilder = new StringBuilder()
-                .append(IconDB.COLUMN_ROWID)
-                .append(" IN (");
-
-        int count = mItemsToDelete.size();
-        for (int i = 0;  i < count; i++) {
-            if (mItemsToDelete.valueAt(i)) {
-                if (deleteCount > 0) {
-                    queryBuilder.append(", ");
-                }
-                queryBuilder.append(mItemsToDelete.keyAt(i));
-                deleteCount++;
-            }
-        }
-        queryBuilder.append(')');
-
-        if (deleteCount > 0) {
-            mIconCache.mIconDb.delete(queryBuilder.toString(), null);
-        }
-    }
-
-
-    /**
-     * A runnable that updates invalid icons and adds missing icons in the DB for the provided
-     * LauncherActivityInfo list. Items are updated/added one at a time, so that the
-     * worker thread doesn't get blocked.
-     */
-    private class SerializedIconUpdateTask<T> implements Runnable {
-        private final long mUserSerial;
-        private final UserHandle mUserHandle;
-        private final Stack<T> mAppsToAdd;
-        private final Stack<T> mAppsToUpdate;
-        private final CachingLogic<T> mCachingLogic;
-        private final HashSet<String> mUpdatedPackages = new HashSet<>();
-        private final OnUpdateCallback mOnUpdateCallback;
-
-        SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
-                Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic,
-                OnUpdateCallback onUpdateCallback) {
-            mUserHandle = userHandle;
-            mUserSerial = userSerial;
-            mAppsToAdd = appsToAdd;
-            mAppsToUpdate = appsToUpdate;
-            mCachingLogic = cachingLogic;
-            mOnUpdateCallback = onUpdateCallback;
-        }
-
-        @Override
-        public void run() {
-            if (!mAppsToUpdate.isEmpty()) {
-                T app = mAppsToUpdate.pop();
-                String pkg = mCachingLogic.getComponent(app).getPackageName();
-                PackageInfo info = mPkgInfoMap.get(pkg);
-                mIconCache.addIconToDBAndMemCache(
-                        app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
-                mUpdatedPackages.add(pkg);
-
-                if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
-                    // No more app to update. Notify callback.
-                    mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
-                }
-
-                // Let it run one more time.
-                scheduleNext();
-            } else if (!mAppsToAdd.isEmpty()) {
-                T app = mAppsToAdd.pop();
-                PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
-                // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
-                // app should have package info, this is not guaranteed by the api
-                if (info != null) {
-                    mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
-                            mUserSerial, false /*replace existing*/);
-                }
-
-                if (!mAppsToAdd.isEmpty()) {
-                    scheduleNext();
-                }
-            }
-        }
-
-        public void scheduleNext() {
-            mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
-                    SystemClock.uptimeMillis() + 1);
-        }
-    }
-
-    public interface OnUpdateCallback {
-
-        void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user);
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
deleted file mode 100644
index 34bed94..0000000
--- a/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.android.launcher3.util;
-
-/**
- * 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.
- */
-
-import android.content.ComponentName;
-import android.os.UserHandle;
-
-import java.util.Arrays;
-
-public class ComponentKey {
-
-    public final ComponentName componentName;
-    public final UserHandle user;
-
-    private final int mHashCode;
-
-    public ComponentKey(ComponentName componentName, UserHandle user) {
-        if (componentName == null || user == null) {
-            throw new NullPointerException();
-        }
-        this.componentName = componentName;
-        this.user = user;
-        mHashCode = Arrays.hashCode(new Object[] {componentName, user});
-
-    }
-
-    @Override
-    public int hashCode() {
-        return mHashCode;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        ComponentKey other = (ComponentKey) o;
-        return other.componentName.equals(componentName) && other.user.equals(user);
-    }
-
-    /**
-     * Encodes a component key as a string of the form [flattenedComponentString#userId].
-     */
-    @Override
-    public String toString() {
-        return componentName.flattenToString() + "#" + user;
-    }
-}
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
deleted file mode 100644
index fe864a2..0000000
--- a/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.database.DatabaseErrorHandler;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDatabase.CursorFactory;
-import android.database.sqlite.SQLiteDatabase.OpenParams;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.Build;
-
-/**
- * Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by
- * A context wrapper which creates databases without support for localized collators.
- */
-public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
-
-    private static final boolean ATLEAST_P =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
-
-    public NoLocaleSQLiteHelper(Context context, String name, int version) {
-        super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version);
-        if (ATLEAST_P) {
-            setOpenParams(new OpenParams.Builder().addOpenFlags(NO_LOCALIZED_COLLATORS).build());
-        }
-    }
-
-    private static class NoLocalContext extends ContextWrapper {
-        public NoLocalContext(Context base) {
-            super(base);
-        }
-
-        @Override
-        public SQLiteDatabase openOrCreateDatabase(
-                String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
-            return super.openOrCreateDatabase(
-                    name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler);
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
deleted file mode 100644
index 49de4bd..0000000
--- a/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.android.launcher3.util;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteFullException;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
-/**
- * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB.
- * Any exception during write operations are ignored, and any version change causes a DB reset.
- */
-public abstract class SQLiteCacheHelper {
-    private static final String TAG = "SQLiteCacheHelper";
-
-    private static final boolean IN_MEMORY_CACHE = false;
-
-    private final String mTableName;
-    private final MySQLiteOpenHelper mOpenHelper;
-
-    private boolean mIgnoreWrites;
-
-    public SQLiteCacheHelper(Context context, String name, int version, String tableName) {
-        if (IN_MEMORY_CACHE) {
-            name = null;
-        }
-        mTableName = tableName;
-        mOpenHelper = new MySQLiteOpenHelper(context, name, version);
-
-        mIgnoreWrites = false;
-    }
-
-    /**
-     * @see SQLiteDatabase#delete(String, String, String[])
-     */
-    public void delete(String whereClause, String[] whereArgs) {
-        if (mIgnoreWrites) {
-            return;
-        }
-        try {
-            mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs);
-        } catch (SQLiteFullException e) {
-            onDiskFull(e);
-        } catch (SQLiteException e) {
-            Log.d(TAG, "Ignoring sqlite exception", e);
-        }
-    }
-
-    /**
-     * @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int)
-     */
-    public void insertOrReplace(ContentValues values) {
-        if (mIgnoreWrites) {
-            return;
-        }
-        try {
-            mOpenHelper.getWritableDatabase().insertWithOnConflict(
-                    mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE);
-        } catch (SQLiteFullException e) {
-            onDiskFull(e);
-        } catch (SQLiteException e) {
-            Log.d(TAG, "Ignoring sqlite exception", e);
-        }
-    }
-
-    private void onDiskFull(SQLiteFullException e) {
-        Log.e(TAG, "Disk full, all write operations will be ignored", e);
-        mIgnoreWrites = true;
-    }
-
-    /**
-     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
-     */
-    public Cursor query(String[] columns, String selection, String[] selectionArgs) {
-        return mOpenHelper.getReadableDatabase().query(
-                mTableName, columns, selection, selectionArgs, null, null, null);
-    }
-
-    public void clear() {
-        mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
-    }
-
-    public void close() {
-        mOpenHelper.close();
-    }
-
-    protected abstract void onCreateTable(SQLiteDatabase db);
-
-    /**
-     * A private inner class to prevent direct DB access.
-     */
-    private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper {
-
-        public MySQLiteOpenHelper(Context context, String name, int version) {
-            super(context, name, version);
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            onCreateTable(db);
-        }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        @Override
-        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        private void clearDB(SQLiteDatabase db) {
-            db.execSQL("DROP TABLE IF EXISTS " + mTableName);
-            onCreate(db);
-        }
-    }
-}
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
deleted file mode 100644
index 48f11fd..0000000
--- a/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.icons;
-
-import android.content.Context;
-
-/**
- * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
- * that are threadsafe.
- */
-public class IconFactory extends BaseIconFactory {
-
-    private static final Object sPoolSync = new Object();
-    private static IconFactory sPool;
-    private static int sPoolId = 0;
-
-    /**
-     * Return a new Message instance from the global pool. Allows us to
-     * avoid allocating new objects in many cases.
-     */
-    public static IconFactory obtain(Context context) {
-        int poolId;
-        synchronized (sPoolSync) {
-            if (sPool != null) {
-                IconFactory m = sPool;
-                sPool = m.next;
-                m.next = null;
-                return m;
-            }
-            poolId = sPoolId;
-        }
-
-        return new IconFactory(context,
-                context.getResources().getConfiguration().densityDpi,
-                context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
-                poolId);
-    }
-
-    public static void clearPool() {
-        synchronized (sPoolSync) {
-            sPool = null;
-            sPoolId++;
-        }
-    }
-
-    private final int mPoolId;
-
-    private IconFactory next;
-
-    private IconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
-        super(context, fillResIconDpi, iconBitmapSize);
-        mPoolId = poolId;
-    }
-
-    /**
-     * Recycles a LauncherIcons that may be in-use.
-     */
-    public void recycle() {
-        synchronized (sPoolSync) {
-            if (sPoolId != mPoolId) {
-                return;
-            }
-            // Clear any temporary state variables
-            clear();
-
-            next = sPool;
-            sPool = this;
-        }
-    }
-
-    @Override
-    public void close() {
-        recycle();
-    }
-}
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
deleted file mode 100644
index 1337975..0000000
--- a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons;
-
-import static android.content.Intent.ACTION_MANAGED_PROFILE_ADDED;
-import static android.content.Intent.ACTION_MANAGED_PROFILE_REMOVED;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.SparseLongArray;
-
-import com.android.launcher3.icons.cache.BaseIconCache;
-
-/**
- * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
- * that are threadsafe.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class SimpleIconCache extends BaseIconCache {
-
-    private static SimpleIconCache sIconCache = null;
-    private static final Object CACHE_LOCK = new Object();
-
-    private final SparseLongArray mUserSerialMap = new SparseLongArray(2);
-    private final UserManager mUserManager;
-
-    public SimpleIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi,
-            int iconPixelSize, boolean inMemoryCache) {
-        super(context, dbFileName, bgLooper, iconDpi, iconPixelSize, inMemoryCache);
-        mUserManager = context.getSystemService(UserManager.class);
-
-        // Listen for user cache changes.
-        IntentFilter filter = new IntentFilter(ACTION_MANAGED_PROFILE_ADDED);
-        filter.addAction(ACTION_MANAGED_PROFILE_REMOVED);
-        context.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                resetUserCache();
-            }
-        }, filter, null, new Handler(bgLooper), 0);
-    }
-
-    @Override
-    protected long getSerialNumberForUser(UserHandle user) {
-        synchronized (mUserSerialMap) {
-            int index = mUserSerialMap.indexOfKey(user.getIdentifier());
-            if (index >= 0) {
-                return mUserSerialMap.valueAt(index);
-            }
-            long serial = mUserManager.getSerialNumberForUser(user);
-            mUserSerialMap.put(user.getIdentifier(), serial);
-            return serial;
-        }
-    }
-
-    private void resetUserCache() {
-        synchronized (mUserSerialMap) {
-            mUserSerialMap.clear();
-        }
-    }
-
-    @Override
-    protected boolean isInstantApp(ApplicationInfo info) {
-        return info.isInstantApp();
-    }
-
-    @Override
-    protected BaseIconFactory getIconFactory() {
-        return IconFactory.obtain(mContext);
-    }
-
-    public static SimpleIconCache getIconCache(Context context) {
-        synchronized (CACHE_LOCK) {
-            if (sIconCache != null) {
-                return sIconCache;
-            }
-            boolean inMemoryCache =
-                    context.getResources().getBoolean(R.bool.simple_cache_enable_im_memory);
-            String dbFileName = context.getString(R.string.cache_db_name);
-
-            HandlerThread bgThread = new HandlerThread("simple-icon-cache");
-            bgThread.start();
-
-            sIconCache = new SimpleIconCache(context.getApplicationContext(), dbFileName,
-                    bgThread.getLooper(), context.getResources().getConfiguration().densityDpi,
-                    context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
-                    inMemoryCache);
-            return sIconCache;
-        }
-    }
-}
diff --git a/proguard.flags b/proguard.flags
index 272ab7a..37b8093 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -2,12 +2,6 @@
   *;
 }
 
-# Proguard will strip new callbacks in LauncherApps.Callback from
-# WrappedCallback if compiled against an older SDK. Don't let this happen.
--keep class com.android.launcher3.compat.** {
-  *;
-}
-
 -keep class com.android.launcher3.graphics.ShadowDrawable {
   public <init>(...);
 }
@@ -23,7 +17,10 @@
 # support jar.
 -keep class androidx.recyclerview.widget.RecyclerView { *; }
 
-# Preference fragments
+# Fragments
+-keep class ** extends androidx.fragment.app.Fragment {
+    public <init>(...);
+}
 -keep class ** extends android.app.Fragment {
     public <init>(...);
 }
@@ -50,4 +47,16 @@
 -dontwarn android.app.**
 -dontwarn android.view.**
 -dontwarn android.os.**
--dontwarn android.graphics.**
\ No newline at end of file
+-dontwarn android.graphics.**
+
+# Ignore warnings for hidden utility classes referenced from the shared lib
+-dontwarn com.android.internal.util.**
+
+################ Do not optimize recents lib #############
+-keep class com.android.systemui.** {
+  *;
+}
+
+-keep class com.android.quickstep.** {
+  *;
+}
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
new file mode 100644
index 0000000..5611969
--- /dev/null
+++ b/protos/launcher_atom.proto
@@ -0,0 +1,272 @@
+/*
+ * 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 = "LauncherAtom";
+
+//
+// ItemInfos
+message ItemInfo {
+  oneof Item {
+    Application application = 1;
+    Task task = 2;
+    Shortcut shortcut = 3;
+    Widget widget = 4;
+    FolderIcon folder_icon = 9;
+  }
+  // When used for launch event, stores the global predictive rank
+  optional int32 rank = 5;
+
+  // Stores whether the Item belows to non primary user
+  optional bool is_work = 6;
+
+  // Item can be child node to parent container or parent containers (nested)
+  optional ContainerInfo container_info = 7;
+
+  // Stores the origin of the Item
+  optional Attribute attribute = 8;
+}
+
+// Represents various launcher surface where items are placed.
+message ContainerInfo {
+  oneof Container {
+    WorkspaceContainer workspace = 1;
+    HotseatContainer hotseat = 2;
+    FolderContainer folder = 3;
+    AllAppsContainer all_apps_container = 4;
+    WidgetsContainer widgets_container = 5;
+    PredictionContainer prediction_container = 6;
+    SearchResultContainer search_result_container = 7;
+    ShortcutsContainer shortcuts_container = 8;
+    SettingsContainer settings_container = 9;
+    PredictedHotseatContainer predicted_hotseat_container = 10;
+    TaskSwitcherContainer task_switcher_container = 11;
+  }
+}
+
+// Represents the apps list sorted alphabetically inside the all-apps view.
+message AllAppsContainer {
+}
+
+message WidgetsContainer {
+}
+
+// Represents the predicted apps row(top row) in the all-apps view.
+message PredictionContainer {
+}
+
+// Represents the apps container within search results.
+message SearchResultContainer {
+
+  // Length of search term.
+  optional int32 query_length = 1;
+
+  // Container from where search was invoked.
+  oneof ParentContainer {
+    WorkspaceContainer workspace = 2;
+    AllAppsContainer all_apps_container = 3;
+  }
+}
+
+// Container for package specific shortcuts to deep links and notifications.
+// Typically shown as popup window by longpressing on an icon.
+message ShortcutsContainer {
+}
+
+// Container for generic system shortcuts for launcher specific settings.
+// Typically shown up as popup window by longpressing on empty space on workspace.
+message SettingsContainer {
+}
+
+message TaskSwitcherContainer {
+}
+
+enum Attribute {
+  UNKNOWN = 0;
+  DEFAULT_LAYOUT = 1;       // icon automatically placed in workspace, folder, hotseat
+  BACKUP_RESTORE = 2;       // icon layout restored from backup
+  PINITEM = 3;              // from another app (e.g., Chrome's "Add to Home screen")
+  ALLAPPS_ATOZ = 4;         // within launcher surface, all aps a-z
+  WIDGETS = 5;              // within launcher, widgets tray
+  ADD_TO_HOMESCREEN = 6;    // play install + launcher home setting
+  ALLAPPS_PREDICTION = 7;   // from prediction bar in all apps container
+  HOTSEAT_PREDICTION = 8;   // from prediction bar in hotseat container
+
+  // Folder's label is one of the non-empty suggested values.
+  SUGGESTED_LABEL = 9;
+
+  // Folder's label is non-empty, manually entered by the user
+  // and different from any of suggested values.
+  MANUAL_LABEL = 10;
+
+  // Folder's label is not yet assigned( i.e., title == null).
+  // Eligible for auto-labeling.
+  UNLABELED = 11;
+
+  // Folder's label is empty(i.e., title == "").
+  // Not eligible for auto-labeling.
+  EMPTY_LABEL = 12;
+}
+
+// Main app icons
+message Application {
+  optional string package_name = 1;
+  optional string component_name = 2;
+}
+
+// Legacy shortcuts and shortcuts handled by ShortcutManager
+message Shortcut {
+  optional string shortcut_name = 1;
+}
+
+// AppWidgets handled by AppWidgetManager
+message Widget {
+  optional int32 span_x = 1 [default = 1];
+  optional int32 span_y = 2 [default = 1];
+  optional int32 app_widget_id = 3;
+  optional string package_name = 4; // only populated during snapshot if from workspace
+  optional string component_name = 5; // only populated during snapshot if from workspace
+}
+
+// Tasks handled by PackageManager
+message Task {
+  optional string package_name = 1;
+  optional string component_name = 2;
+  optional int32 index = 3;
+}
+
+// Represents folder in a closed state.
+message FolderIcon {
+  // Number of items inside folder.
+  optional int32 cardinality = 1;
+
+  // State of the folder label before the event.
+  optional FromState from_label_state = 2;
+
+  // State of the folder label after the event.
+  optional ToState to_label_state = 3;
+
+  // Details about actual folder label.
+  // Populated when folder label is not a PII.
+  optional string label_info = 4;
+}
+
+//////////////////////////////////////////////
+// Containers
+
+message WorkspaceContainer {
+  optional int32 page_index = 1 [default = -2]; // range [-1, l], 0 is the index of the main homescreen
+  optional int32 grid_x = 2 [default = -1]; // [0, m], m varies based on the display density and resolution
+  optional int32 grid_y = 3 [default = -1]; // [0, n], n varies based on the display density and resolution
+}
+
+message HotseatContainer {
+  optional int32 index = 1;
+}
+
+// Represents hotseat container with prediction feature enabled.
+message PredictedHotseatContainer {
+  optional int32 index = 1;
+
+  // No of hotseat positions filled with predicted items.
+  optional int32 cardinality = 2;
+}
+
+message FolderContainer {
+  optional int32 page_index = 1 [default = -1];
+  optional int32 grid_x = 2 [default = -1];
+  optional int32 grid_y = 3 [default = -1];
+  oneof ParentContainer {
+    WorkspaceContainer workspace = 4;
+    HotseatContainer hotseat = 5;
+  }
+}
+
+// Represents state of EditText field before update.
+enum FromState {
+  // Default value.
+  // Used when a FromState is not applicable, for example, during folder creation.
+  FROM_STATE_UNSPECIFIED = 0;
+
+  // EditText was empty.
+  // Eg: When a folder label is updated from empty string.
+  FROM_EMPTY = 1;
+
+  // EditText was non-empty and manually entered by the user.
+  // Eg: When a folder label is updated from a user-entered value.
+  FROM_CUSTOM = 2;
+
+  // EditText was non-empty and one of the suggestions.
+  // Eg: When a folder label is updated from a suggested value.
+  FROM_SUGGESTED = 3;
+}
+
+// Represents state of EditText field after update.
+enum ToState {
+  // Default value.
+  // Used when ToState is not applicable, for example, when folder label is updated to a different
+  // value when folder label suggestion feature is disabled.
+  TO_STATE_UNSPECIFIED = 0;
+
+  // User attempted to change the EditText, but was not changed.
+  UNCHANGED = 1;
+
+  // New label matches with primary(aka top) suggestion.
+  TO_SUGGESTION0 = 2;
+
+  // New value matches with second top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION1_WITH_VALID_PRIMARY = 3;
+
+  // New value matches with second top suggestion given that top suggestion was empty.
+  TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 4;
+
+  // New value matches with third top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION2_WITH_VALID_PRIMARY = 5;
+
+  // New value matches with third top suggestion given that top suggestion was empty.
+  TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 6;
+
+  // New value matches with 4th top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION3_WITH_VALID_PRIMARY = 7;
+
+  // New value matches with 4th top suggestion given that top suggestion was empty.
+  TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 8;
+
+  // New value is empty even though the top suggestion was non-empty.
+  TO_EMPTY_WITH_VALID_PRIMARY = 9;
+
+  // New value is empty given that top suggestion was empty.
+  TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 10;
+
+  // New value is empty given that no suggestions were provided.
+  TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 11;
+
+  // New value is empty given that suggestions feature was disabled.
+  TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 12;
+
+  // New value is non-empty and does not match with any of the suggestions even though the top suggestion was non-empty.
+  TO_CUSTOM_WITH_VALID_PRIMARY = 13;
+
+  // New value is non-empty and not match with any suggestions given that top suggestion was empty.
+  TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 14;
+
+  // New value is non-empty and also no suggestions were provided.
+  TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 15;
+
+  // New value is non-empty and also suggestions feature was disable.
+  TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 16;
+}
diff --git a/protos/launcher_dump.proto b/protos/launcher_dump.proto
deleted file mode 100644
index dc8fbda..0000000
--- a/protos/launcher_dump.proto
+++ /dev/null
@@ -1,75 +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.
- */
-syntax = "proto2";
-
-option java_package = "com.android.launcher3.model";
-option java_outer_classname = "LauncherDumpProto";
-
-package model;
-
-message DumpTarget {
-  enum Type {
-    NONE = 0;
-    ITEM = 1;
-    CONTAINER = 2;
-  }
-
-  optional Type type = 1;
-  optional int32 page_id = 2;
-  optional int32 grid_x = 3;
-  optional int32 grid_y = 4;
-
-  // For container types only
-  optional ContainerType container_type = 5;
-
-  // For item types only
-  optional ItemType item_type = 6;
-
-  optional string package_name = 7; // All ItemTypes except UNKNOWN type
-  optional string component = 8;   // All ItemTypes except UNKNOWN type
-  optional string item_id = 9; // For Pinned Shortcuts and appWidgetId
-
-  optional int32 span_x = 10 [default = 1];// Used for ItemType.WIDGET
-  optional int32 span_y = 11 [default = 1];// Used for ItemType.WIDGET
-  optional UserType user_type = 12;
-}
-
-// Used to define what type of item a Target would represent.
-enum ItemType {
-  UNKNOWN_ITEMTYPE = 0;  // Launcher specific items
-  APP_ICON = 1; // Regular app icons
-  WIDGET = 2;   // Elements from AppWidgetManager
-  SHORTCUT = 3; // ShortcutManager
-}
-
-// Used to define what type of container a Target would represent.
-enum ContainerType {
-  UNKNOWN_CONTAINERTYPE = 0;
-  WORKSPACE = 1;
-  HOTSEAT = 2;
-  FOLDER = 3;
-}
-
-// Used to define what type of control a Target would represent.
-enum UserType {
-  DEFAULT = 0;
-  WORK = 1;
-}
-
-// Main message;
-message LauncherImpression {
-  repeated DumpTarget targets = 1;
-}
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index fd36d4b..9423cb2 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -57,6 +57,40 @@
   optional TargetExtension extension = 16;
   optional TipType tip_type = 17;
   optional int32 search_query_length = 18;
+  optional bool is_work_app = 19;
+  optional FromFolderLabelState from_folder_label_state = 20;
+  optional ToFolderLabelState to_folder_label_state = 21;
+
+  // Note: proto does not support duplicate enum values, even if they belong to different enum type.
+  // Hence "FROM" and "TO" prefix added.
+  enum FromFolderLabelState {
+    FROM_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
+    FROM_EMPTY = 1;
+    FROM_CUSTOM = 2;
+    FROM_SUGGESTED = 3;
+  }
+
+  enum ToFolderLabelState {
+    TO_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
+    TO_SUGGESTION0_WITH_VALID_PRIMARY = 1;
+    TO_SUGGESTION1_WITH_VALID_PRIMARY = 2;
+    TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 3;
+    TO_SUGGESTION2_WITH_VALID_PRIMARY = 4;
+    TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 5;
+    TO_SUGGESTION3_WITH_VALID_PRIMARY = 6;
+    TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 7;
+    TO_EMPTY_WITH_VALID_SUGGESTIONS = 8 [deprecated = true];
+    TO_EMPTY_WITH_VALID_PRIMARY = 15;
+    TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 16;
+    TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 9;
+    TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 10;
+    TO_CUSTOM_WITH_VALID_SUGGESTIONS = 11 [deprecated = true];
+    TO_CUSTOM_WITH_VALID_PRIMARY = 17;
+    TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 18;
+    TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 12;
+    TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 13;
+    UNCHANGED = 14;
+  }
 }
 
 // Used to define what type of item a Target would represent.
@@ -92,7 +126,7 @@
   TASKSWITCHER = 12; // Recents UI Container (QuickStep)
   APP = 13; // Foreground activity is another app (QuickStep)
   TIP = 14; // Onboarding texts (QuickStep)
-  SIDELOADED_LAUNCHER = 15;
+  OTHER_LAUNCHER_APP = 15;
 }
 
 // Used to define what type of control a Target would represent.
@@ -119,6 +153,13 @@
   BACK_GESTURE = 19;
   UNDO = 20;
   DISMISS_PREDICTION = 21;
+  HYBRID_HOTSEAT_ACCEPTED = 22;
+  HYBRID_HOTSEAT_CANCELED = 23;
+  OVERVIEW_ACTIONS_SHARE_BUTTON = 24;
+  OVERVIEW_ACTIONS_SCREENSHOT_BUTTON = 25;
+  OVERVIEW_ACTIONS_SELECT_BUTTON = 26;
+  SELECT_MODE_CLOSE_BUTTON = 27;
+  SELECT_MODE_ITEM = 28;
 }
 
 enum TipType {
@@ -128,6 +169,7 @@
   QUICK_SCRUB_TEXT = 3;
   PREDICTION_TEXT = 4;
   DWB_TOAST = 5;
+  HYBRID_HOTSEAT = 6;
 }
 
 // Used to define the action component of the LauncherEvent.
diff --git a/src/com/android/launcher3/views/Transposable.java b/protos/launcher_trace.proto
similarity index 67%
copy from src/com/android/launcher3/views/Transposable.java
copy to protos/launcher_trace.proto
index 929c1aa..c6f3543 100644
--- a/src/com/android/launcher3/views/Transposable.java
+++ b/protos/launcher_trace.proto
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,14 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.views;
 
-import com.android.launcher3.graphics.RotationMode;
+syntax = "proto2";
 
-/**
- * Indicates that a view can be transposed.
- */
-public interface Transposable {
+package com.android.launcher3.tracing;
 
-    RotationMode getRotationMode();
+option java_multiple_files = true;
+
+message LauncherTraceProto {
+
+    optional TouchInteractionServiceProto touch_interaction_service = 1;
+}
+
+message TouchInteractionServiceProto {
+
+    optional bool service_connected = 1;
 }
diff --git a/protos/launcher_trace_file.proto b/protos/launcher_trace_file.proto
new file mode 100644
index 0000000..6ce182a
--- /dev/null
+++ b/protos/launcher_trace_file.proto
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+import "launcher_trace.proto";
+
+package com.android.launcher3.tracing;
+
+option java_multiple_files = true;
+
+/* represents a file full of launcher trace entries.
+   Encoded, it should start with 0x9 0x4C 0x4E 0x43 0x48 0x52 0x54 0x52 0x43 (.LNCHRTRC), such
+   that they can be easily identified. */
+message LauncherTraceFileProto {
+
+    /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+       (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+        constants into .proto files. */
+    enum MagicNumber {
+        INVALID = 0;
+        MAGIC_NUMBER_L = 0x48434E4C;  /* LNCH (little-endian ASCII) */
+        MAGIC_NUMBER_H = 0x43525452;  /* RTRC (little-endian ASCII) */
+    }
+
+    optional fixed64 magic_number = 1;  /* Must be the first field, set to value in MagicNumber */
+    repeated LauncherTraceEntryProto entry = 2;
+}
+
+/* one launcher trace entry. */
+message LauncherTraceEntryProto {
+    /* required: elapsed realtime in nanos since boot of when this entry was logged */
+    optional fixed64 elapsed_realtime_nanos = 1;
+
+    optional LauncherTraceProto launcher = 3;
+}
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
new file mode 100644
index 0000000..60afddb
--- /dev/null
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2019, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.launcher3">
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+    <!--
+    Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
+    Refer comments around specific entries on how to extend individual components.
+    -->
+
+    <application
+        android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+        android:fullBackupOnly="true"
+        android:fullBackupContent="@xml/backupscheme"
+        android:hardwareAccelerated="true"
+        android:icon="@drawable/ic_launcher_home"
+        android:label="@string/derived_app_name"
+        android:theme="@style/AppTheme"
+        android:largeHeap="@bool/config_largeHeap"
+        android:restoreAnyVersion="true"
+        android:supportsRtl="true" >
+
+        <!--
+        Main launcher activity. When extending only change the name, and keep all the
+        attributes and intent filters the same
+        -->
+        <activity
+            android:name="com.android.launcher3.uioverrides.QuickstepLauncher"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:stateNotNeeded="true"
+            android:windowSoftInputMode="adjustPan"
+            android:screenOrientation="unspecified"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:resizeableActivity="true"
+            android:resumeWhilePausing="true"
+            android:taskAffinity=""
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.HOME" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.MONKEY"/>
+                <category android:name="android.intent.category.LAUNCHER_APP" />
+            </intent-filter>
+            <meta-data
+                android:name="com.android.launcher3.grid.control"
+                android:value="${packageName}.grid_control" />
+        </activity>
+
+    </application>
+</manifest>
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 826a275..e49f2ec 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -22,8 +22,15 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3" >
 
+    <permission
+        android:name="${packageName}.permission.HOTSEAT_EDU"
+        android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
+        android:protectionLevel="signatureOrSystem" />
+
     <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="${packageName}.permission.HOTSEAT_EDU" />
 
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
@@ -46,10 +53,7 @@
             </intent-filter>
         </service>
 
-        <!-- STOPSHIP: Change exported to false once all the integration is complete.
-        It is set to true so that the activity can be started from command line -->
         <activity android:name="com.android.quickstep.RecentsActivity"
-            android:exported="true"
             android:excludeFromRecents="true"
             android:launchMode="singleTask"
             android:clearTaskOnLaunch="true"
@@ -74,6 +78,17 @@
             </intent-filter>
         </provider>
 
+        <!-- FileProvider used for sharing images. -->
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${packageName}.overview.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/overview_file_provider_paths" />
+        </provider>
+
         <service
             android:name="com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL$ColorExtractionService"
             tools:node="remove" />
@@ -85,11 +100,28 @@
             android:clearTaskOnLaunch="true"
             android:exported="false" />
 
-        <activity android:name="com.android.quickstep.LockScreenRecentsActivity"
-                  android:theme="@android:style/Theme.NoDisplay"
-                  android:showOnLockScreen="true"
-                  android:taskAffinity="${packageName}.locktask"
-                  android:directBootAware="true" />
+        <activity
+            android:name="com.android.quickstep.interaction.GestureSandboxActivity"
+            android:autoRemoveFromRecents="true"
+            android:excludeFromRecents="true"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="com.android.quickstep.action.GESTURE_SANDBOX" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".hybridhotseat.HotseatEduActivity"
+            android:theme="@android:style/Theme.NoDisplay"
+            android:noHistory="true"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:permission="${packageName}.permission.HOTSEAT_EDU">
+            <intent-filter>
+                <action android:name="com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU"/>
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
     </application>
 
diff --git a/go/quickstep/res/drawable/clear_all_button.xml b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
similarity index 73%
copy from go/quickstep/res/drawable/clear_all_button.xml
copy to quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
index acac32d..df7cd8e 100644
--- a/go/quickstep/res/drawable/clear_all_button.xml
+++ b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -16,7 +15,8 @@
 -->
 <shape
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@color/clear_all_button_bg"/>
-    <corners android:radius="4dp"/>
-</shape>
+    android:shape="oval">
+    <solid android:color="?android:colorAccent"/>
+    <size android:height="@dimen/swipe_edu_circle_size"
+        android:width="@dimen/swipe_edu_circle_size" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml b/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml
new file mode 100644
index 0000000..5a2dfb7
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml
@@ -0,0 +1,8 @@
+<?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/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
new file mode 100644
index 0000000..4fda2a9
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:height="24dp" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="?android:attr/colorAccent" android:pathData="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/>
+</vector>
diff --git a/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml b/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml
new file mode 100644
index 0000000..e7ef6e6
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.quickstep.views.AllAppsEduView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="@dimen/swipe_edu_width"
+    android:layout_height="@dimen/swipe_edu_max_height"/>
diff --git a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
index ef272ed..cd64a94 100644
--- a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
+++ b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
@@ -13,19 +13,30 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.fallback.RecentsRootView
+<com.android.launcher3.LauncherRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/drag_layer"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true">
 
-    <com.android.quickstep.fallback.FallbackRecentsView
-        android:id="@id/overview_panel"
+    <com.android.quickstep.fallback.RecentsDragLayer
+        android:id="@+id/drag_layer"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:outlineProvider="none"
-        android:theme="@style/HomeScreenElementTheme" />
-</com.android.quickstep.fallback.RecentsRootView>
+        android:clipChildren="false">
+
+        <com.android.quickstep.fallback.FallbackRecentsView
+            android:id="@id/overview_panel"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:outlineProvider="none"
+            android:theme="@style/HomeScreenElementTheme" />
+
+        <include
+            android:id="@+id/overview_actions_view"
+            layout="@layout/overview_actions_container" />
+
+    </com.android.quickstep.fallback.RecentsDragLayer>
+</com.android.launcher3.LauncherRootView>
diff --git a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
index 7f1425b..fe57e9b 100644
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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");
@@ -14,12 +13,20 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.views.LauncherRecentsView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:theme="@style/HomeScreenElementTheme"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:accessibilityPaneTitle="@string/accessibility_recent_apps"
-    android:visibility="invisible" />
\ No newline at end of file
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <com.android.quickstep.views.LauncherRecentsView
+        android:id="@+id/overview_panel"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:accessibilityPaneTitle="@string/accessibility_recent_apps"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:theme="@style/HomeScreenElementTheme"
+        android:visibility="invisible" />
+
+    <include
+        android:id="@+id/overview_actions_view"
+        layout="@layout/overview_actions_container" />
+
+</merge>
diff --git a/iconloaderlib/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
similarity index 80%
rename from iconloaderlib/res/values/dimens.xml
rename to quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
index e8c0c44..70a765a 100644
--- a/iconloaderlib/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -14,6 +14,4 @@
      limitations under the License.
 -->
 
-<resources>
-    <dimen name="profile_badge_size">24dp</dimen>
-</resources>
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace" />
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
new file mode 100644
index 0000000..1dab482
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.hybridhotseat.HotseatEduDialog xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_gravity="bottom"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:orientation="vertical">
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="32dp"
+        android:backgroundTint="?attr/eduHalfSheetBGColor"
+        android:background="@drawable/bottom_sheet_top_border" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="?attr/eduHalfSheetBGColor"
+        android:orientation="vertical">
+
+        <TextView
+            style="@style/TextHeadline"
+            android:id="@+id/hotseat_edu_heading"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="18dp"
+            android:paddingLeft="@dimen/bottom_sheet_edu_padding"
+            android:paddingRight="@dimen/bottom_sheet_edu_padding"
+            android:text="@string/hotseat_edu_title_migrate"
+            android:textAlignment="center"
+            android:textColor="@android:color/white"
+            android:textSize="20sp" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:id="@+id/hotseat_edu_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="18dp"
+            android:layout_marginBottom="18dp"
+            android:fontFamily="roboto-medium"
+            android:paddingLeft="@dimen/bottom_sheet_edu_padding"
+            android:paddingRight="@dimen/bottom_sheet_edu_padding"
+            android:text="@string/hotseat_edu_message_migrate"
+            android:textAlignment="center"
+            android:textColor="@android:color/white"
+            android:textSize="16sp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/hotseat_wrapper"
+            android:orientation="vertical">
+
+            <com.android.launcher3.CellLayout
+                android:id="@+id/sample_prediction"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                launcher:containerType="hotseat" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/bottom_sheet_edu_padding"
+                android:paddingTop="8dp"
+                android:paddingRight="@dimen/bottom_sheet_edu_padding">
+
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight=".4">
+                    <Button
+                        android:id="@+id/no_thanks"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="?android:attr/selectableItemBackground"
+                        android:text="@string/hotseat_edu_dismiss"
+                        android:layout_gravity="start|center_vertical"
+                        android:textColor="@android:color/white"/>
+                </FrameLayout>
+                <FrameLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight=".6">
+                    <Button
+                        android:id="@+id/turn_predictions_on"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="?android:attr/selectableItemBackground"
+                        android:layout_gravity="end|center_vertical"
+                        android:text="@string/hotseat_edu_accept"
+                        android:textColor="@android:color/white"/>
+                </FrameLayout>
+
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+
+</com.android.launcher3.hybridhotseat.HotseatEduDialog>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 7426e30..f03f118 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -1,6 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <resources>
     <color name="chip_hint_foreground_color">#fff</color>
+    <color name="chip_scrim_start_color">#39000000</color>
 
     <color name="all_apps_label_text">#61000000</color>
     <color name="all_apps_label_text_dark">#61FFFFFF</color>
diff --git a/iconloaderlib/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/config.xml
similarity index 77%
copy from iconloaderlib/res/values/dimens.xml
copy to quickstep/recents_ui_overrides/res/values/config.xml
index e8c0c44..120e034 100644
--- a/iconloaderlib/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/config.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      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.
 -->
-
 <resources>
-    <dimen name="profile_badge_size">24dp</dimen>
-</resources>
+    <integer name="max_depth_blur_radius">150</integer>
+</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index 863a8ba..9266b06 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -1,4 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <resources>
     <dimen name="chip_hint_border_width">1dp</dimen>
     <dimen name="chip_hint_corner_radius">20dp</dimen>
@@ -6,6 +20,7 @@
     <dimen name="chip_hint_start_padding">10dp</dimen>
     <dimen name="chip_hint_end_padding">12dp</dimen>
     <dimen name="chip_hint_horizontal_margin">20dp</dimen>
+    <dimen name="chip_hint_vertical_offset">16dp</dimen>
     <dimen name="chip_hint_elevation">2dp</dimen>
     <dimen name="chip_icon_size">16dp</dimen>
     <dimen name="chip_text_height">26dp</dimen>
@@ -17,15 +32,7 @@
     <dimen name="all_apps_label_top_padding">16dp</dimen>
     <dimen name="all_apps_label_bottom_padding">8dp</dimen>
     <dimen name="all_apps_label_text_size">14sp</dimen>
-    <dimen name="all_apps_tip_bottom_margin">8dp</dimen>
-    <!-- The size of corner radius of the arrow in the arrow toast. -->
-    <dimen name="arrow_toast_corner_radius">2dp</dimen>
 
     <!-- Minimum distance to swipe to trigger accessibility gesture -->
     <dimen name="accessibility_gesture_min_swipe_distance">80dp</dimen>
-
-    <!-- Swipe up to home related -->
-    <dimen name="swipe_up_fling_min_visible_change">18dp</dimen>
-    <dimen name="swipe_up_y_overshoot">10dp</dimen>
-    <dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index ed3ba92..6aa9619 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -26,5 +26,7 @@
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
 
   <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherAppPredictionExtension</string>
+
+  <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
 </resources>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 114fd8e..38adf39 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -16,41 +16,25 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-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.LauncherStateManager.ANIM_ALL;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
-import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -61,13 +45,6 @@
  */
 public final class LauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
 
-    public static final int INDEX_SHELF_ANIM = 0;
-    public static final int INDEX_RECENTS_FADE_ANIM = 1;
-    public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = 2;
-    public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM = 3;
-
-    public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
-
     public LauncherAppTransitionManagerImpl(Context context) {
         super(context);
     }
@@ -81,22 +58,23 @@
 
     @Override
     protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
-            @NonNull RemoteAnimationTargetCompat[] targets, boolean launcherClosing) {
+            @NonNull RemoteAnimationTargetCompat[] appTargets,
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing) {
         RecentsView recentsView = mLauncher.getOverviewPanel();
         boolean skipLauncherChanges = !launcherClosing;
 
-        TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets);
-
-        ClipAnimationHelper helper = new ClipAnimationHelper(mLauncher);
-        anim.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper)
-                .setDuration(RECENTS_LAUNCH_DURATION));
+        TaskView taskView = findTaskViewToLaunch(mLauncher, v, appTargets);
+        PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+        createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
+                mLauncher.getDepthController(), pa);
+        anim.play(pa.buildAnim());
 
         Animator childStateAnimation = null;
         // Found a visible recents task that matches the opening app, lets launch the app from there
         Animator launcherAnim;
         final AnimatorListenerAdapter windowAnimEndListener;
         if (launcherClosing) {
-            launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper);
+            launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
             launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
             launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
 
@@ -149,74 +127,8 @@
 
         return () -> {
             overview.setFreezeViewVisibility(false);
+            overview.setTranslationY(0);
             mLauncher.getStateManager().reapplyState();
         };
     }
-
-    @Override
-    public int getStateElementAnimationsCount() {
-        return 4;
-    }
-
-    @Override
-    public Animator createStateElementAnimation(int index, float... values) {
-        switch (index) {
-            case INDEX_SHELF_ANIM: {
-                AllAppsTransitionController aatc = mLauncher.getAllAppsController();
-                Animator springAnim = aatc.createSpringAnimation(values);
-
-                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-                    // Translate hotseat with the shelf until reaching overview.
-                    float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
-                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mLauncher);
-                    float shiftRange = aatc.getShiftRange();
-                    if (values.length == 1) {
-                        values = new float[] {aatc.getProgress(), values[0]};
-                    }
-                    ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
-                    hotseatAnim.addUpdateListener(anim -> {
-                        float progress = (Float) anim.getAnimatedValue();
-                        if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
-                            float hotseatShift = (progress - overviewProgress) * shiftRange;
-                            mLauncher.getHotseat().setTranslationY(hotseatShift + sat.translationY);
-                        }
-                    });
-                    hotseatAnim.setInterpolator(LINEAR);
-                    hotseatAnim.setDuration(springAnim.getDuration());
-
-                    AnimatorSet anim = new AnimatorSet();
-                    anim.play(hotseatAnim);
-                    anim.play(springAnim);
-                    return anim;
-                }
-
-                return springAnim;
-            }
-            case INDEX_RECENTS_FADE_ANIM:
-                return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
-                        RecentsView.CONTENT_ALPHA, values);
-            case INDEX_RECENTS_TRANSLATE_X_ANIM:
-                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), VIEW_TRANSLATE_X)
-                        .setDampingRatio(0.8f)
-                        .setStiffness(250)
-                        .setValues(values)
-                        .build(mLauncher);
-            case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
-                AnimatorSetBuilder builder = new AnimatorSetBuilder();
-                builder.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
-                builder.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_3);
-                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-                    builder.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
-                    builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
-                }
-                LauncherStateManager stateManager = mLauncher.getStateManager();
-                return stateManager.createAtomicAnimation(
-                        stateManager.getCurrentStableState(), OVERVIEW, builder,
-                        ANIM_ALL, ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW);
-            }
-
-            default:
-                return super.createStateElementAnimation(index, values);
-        }
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
deleted file mode 100644
index c5c4add..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3;
-
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-
-import java.util.function.BiPredicate;
-
-public class LauncherInitListenerEx extends LauncherInitListener {
-
-    public LauncherInitListenerEx(BiPredicate<Launcher, Boolean> onInitListener) {
-        super(onInitListener);
-    }
-
-    @Override
-    protected boolean init(Launcher launcher, boolean alreadyOnHome) {
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
-        return super.init(launcher, alreadyOnHome);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
index d3042cf..8477b10 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -16,133 +16,30 @@
 
 package com.android.launcher3.appprediction;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
 
-import android.content.Context;
-import android.graphics.CornerPathEffect;
-import android.graphics.Paint;
-import android.graphics.drawable.ShapeDrawable;
-import android.os.Handler;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
+import android.os.UserManager;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.views.ArrowTipView;
 import com.android.systemui.shared.system.LauncherEventUtil;
 
-import androidx.core.content.ContextCompat;
-
 /**
- * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the
+ * ArrowTip helper aligned just above prediction apps, shown to users that enter all apps for the
  * first time.
  */
-public class AllAppsTipView extends AbstractFloatingView {
+public class AllAppsTipView {
 
     private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
-    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
-    private static final long SHOW_DELAY_MS = 200;
-    private static final long SHOW_DURATION_MS = 300;
-    private static final long HIDE_DURATION_MS = 100;
-
-    private final Launcher mLauncher;
-    private final Handler mHandler = new Handler();
-
-    private AllAppsTipView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        setOrientation(LinearLayout.VERTICAL);
-
-        mLauncher = Launcher.getLauncher(context);
-
-        init(context);
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            close(true);
-        }
-        return false;
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        if (mIsOpen) {
-            if (animate) {
-                animate().alpha(0f)
-                        .withLayer()
-                        .setStartDelay(0)
-                        .setDuration(HIDE_DURATION_MS)
-                        .setInterpolator(Interpolators.ACCEL)
-                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
-                        .start();
-            } else {
-                animate().cancel();
-                mLauncher.getDragLayer().removeView(this);
-            }
-            mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
-            mIsOpen = false;
-        }
-    }
-
-    @Override
-    public void logActionCommand(int command) {
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_ON_BOARD_POPUP) != 0;
-    }
-
-    private void init(Context context) {
-        inflate(context, R.layout.arrow_toast, this);
-
-        TextView textView = findViewById(R.id.text);
-        textView.setText(R.string.all_apps_prediction_tip);
-
-        View dismissButton = findViewById(R.id.dismiss);
-        dismissButton.setOnClickListener(view -> {
-            mLauncher.getUserEventDispatcher().logActionTip(
-                    LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS);
-            handleClose(true);
-        });
-
-        View arrowView = findViewById(R.id.arrow);
-        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
-        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                arrowLp.width, arrowLp.height, false));
-        Paint arrowPaint = arrowDrawable.getPaint();
-        TypedValue typedValue = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
-        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
-        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-        arrowPaint.setPathEffect(new CornerPathEffect(
-                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
-        arrowView.setBackground(arrowDrawable);
-
-        mIsOpen = true;
-
-        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
-    }
 
     private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
         FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
@@ -151,33 +48,20 @@
                 TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE) != null
                 || !launcher.isInState(ALL_APPS)
                 || hasSeenAllAppsTip(launcher)
-                || UserManagerCompat.getInstance(launcher).isDemoUser()
+                || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return false;
         }
 
-        AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
-            null);
-        launcher.getDragLayer().addView(allAppsTipView);
+        int[] coords = new int[2];
+        floatingHeaderView.findFixedRowByType(PredictionRowView.class).getLocationOnScreen(coords);
+        ArrowTipView arrowTipView = new ArrowTipView(launcher).setOnClosedCallback(() -> {
+            launcher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
+            launcher.getUserEventDispatcher().logActionTip(LauncherEventUtil.DISMISS,
+                    ALL_APPS_PREDICTION_TIPS);
+        });
+        arrowTipView.show(launcher.getString(R.string.all_apps_prediction_tip), coords[1]);
 
-        DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
-        params.gravity = Gravity.CENTER_HORIZONTAL;
-
-        int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
-        allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
-                R.dimen.all_apps_tip_bottom_margin));
-
-        allAppsTipView.setAlpha(0);
-        allAppsTipView.animate()
-                .alpha(1f)
-                .withLayer()
-                .setStartDelay(SHOW_DELAY_MS)
-                .setDuration(SHOW_DURATION_MS)
-                .setInterpolator(Interpolators.DEACCEL)
-                .start();
-
-        launcher.getUserEventDispatcher().logActionTip(
-                LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
         return true;
     }
 
@@ -187,21 +71,16 @@
 
     public static void scheduleShowIfNeeded(Launcher launcher) {
         if (!hasSeenAllAppsTip(launcher)) {
-            launcher.getStateManager().addStateListener(
-                    new LauncherStateManager.StateListener() {
-                        @Override
-                        public void onStateTransitionStart(LauncherState toState) {
+            launcher.getStateManager().addStateListener(new StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if (finalState == ALL_APPS) {
+                        if (showAllAppsTipIfNecessary(launcher)) {
+                            launcher.getStateManager().removeStateListener(this);
                         }
-
-                        @Override
-                        public void onStateTransitionComplete(LauncherState finalState) {
-                            if (finalState == ALL_APPS) {
-                                if (showAllAppsTipIfNecessary(launcher)) {
-                                    launcher.getStateManager().removeStateListener(this);
-                                }
-                            }
-                        }
-                    });
+                    }
+                }
+            });
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
index 425fb13..914d9e9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.appprediction;
 
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 
 import android.annotation.TargetApi;
@@ -37,18 +38,18 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.Themes;
 
 /**
  * A view which shows a horizontal divider
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class AppsDividerView extends View implements LauncherStateManager.StateListener,
+public class AppsDividerView extends View implements StateListener<LauncherState>,
         FloatingHeaderRow {
 
     private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
@@ -250,9 +251,6 @@
     }
 
     @Override
-    public void onStateTransitionStart(LauncherState toState) { }
-
-    @Override
     public void onStateTransitionComplete(LauncherState finalState) {
         if (finalState == ALL_APPS) {
             setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
@@ -291,7 +289,7 @@
     public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
             PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
         // Don't use setViewAlpha as we want to control the visibility ourselves.
-        setter.setFloat(this, ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade);
+        setter.setFloat(this, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
index b9f4147..d200868 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -18,22 +18,17 @@
 
 import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
-import android.content.Context;
-
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.util.ComponentKey;
 
 public class ComponentKeyMapper {
 
     protected final ComponentKey componentKey;
-    private final Context mContext;
     private final DynamicItemCache mCache;
 
-    public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
-        mContext = context;
+    public ComponentKeyMapper(ComponentKey key, DynamicItemCache cache) {
         componentKey = key;
         mCache = cache;
     }
@@ -61,9 +56,8 @@
             return item;
         } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
             return mCache.getInstantApp(componentKey.componentName.getPackageName());
-        } else if (componentKey instanceof ShortcutKey) {
-            return mCache.getShortcutInfo((ShortcutKey) componentKey);
+        } else {
+            return mCache.getShortcutInfo(componentKey);
         }
-        return null;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 65e69b6..ab96b1340 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.MATCH_INSTANT;
 
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.content.Context;
 import android.content.Intent;
@@ -38,11 +39,13 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 
 import java.util.ArrayList;
@@ -72,8 +75,9 @@
     private final Handler mUiHandler;
     private final InstantAppResolver mInstantAppResolver;
     private final Runnable mOnUpdateCallback;
+    private final IconCache mIconCache;
 
-    private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
+    private final Map<ComponentKey, WorkspaceItemInfo> mShortcuts;
     private final Map<String, InstantAppItemInfo> mInstantApps;
 
     public DynamicItemCache(Context context, Runnable onUpdateCallback) {
@@ -82,6 +86,7 @@
         mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         mInstantAppResolver = InstantAppResolver.newInstance(context);
         mOnUpdateCallback = onUpdateCallback;
+        mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
 
         mShortcuts = new HashMap<>();
         mInstantApps = new HashMap<>();
@@ -162,21 +167,10 @@
 
     @WorkerThread
     private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
-        DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
-        List<ShortcutInfo> details = mgr.queryForFullDetails(
-                shortcutKey.componentName.getPackageName(),
-                Collections.<String>singletonList(shortcutKey.getId()),
-                shortcutKey.user);
+        List<ShortcutInfo> details = shortcutKey.buildRequest(mContext).query(ShortcutRequest.ALL);
         if (!details.isEmpty()) {
             WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
-            try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
-                si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
-            } catch (Exception e) {
-                if (DEBUG) {
-                    Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
-                }
-                return null;
-            }
+            mIconCache.getShortcutIcon(si, details.get(0));
             return si;
         }
         if (DEBUG) {
@@ -209,7 +203,7 @@
         InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
         iconCache.getTitleAndIcon(info, false);
-        if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
+        if (info.bitmap.icon == null || iconCache.isDefaultIcon(info.bitmap, info.user)) {
             return null;
         }
         return info;
@@ -237,7 +231,38 @@
     }
 
     @MainThread
-    public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+    public WorkspaceItemInfo getShortcutInfo(ComponentKey key) {
         return mShortcuts.get(key);
     }
+
+    /**
+     * requests and caches icons for app targets
+     */
+    public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
+            AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
+        List<String> instantAppsToLoad = new ArrayList<>();
+        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
+        int total = componentKeyMappers.size();
+        for (int i = 0, count = 0; i < total && count < itemCount; i++) {
+            ComponentKeyMapper mapper = componentKeyMappers.get(i);
+            // Update instant apps
+            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+                instantAppsToLoad.add(mapper.getPackage());
+                count++;
+            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+                count++;
+            } else {
+                // Reload high res icon
+                AppInfo info = (AppInfo) mapper.getApp(appsStore);
+                if (info != null) {
+                    if (info.usingLowResIcon()) {
+                        mIconCache.updateIconInBackground(callback, info);
+                    }
+                    count++;
+                }
+            }
+        }
+        cacheItems(shortcutsToLoad, instantAppsToLoad);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
index 6e5f461..6c4c601 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
@@ -21,9 +21,9 @@
 import android.content.ComponentName;
 import android.content.Intent;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 public class InstantAppItemInfo extends AppInfo {
 
@@ -44,7 +44,7 @@
         workspaceItemInfo.status = WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON
                 | WorkspaceItemInfo.FLAG_RESTORE_STARTED
                 | WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI;
-        workspaceItemInfo.intent.setPackage(componentName.getPackageName());
+        workspaceItemInfo.getIntent().setPackage(componentName.getPackageName());
         return workspaceItemInfo;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
index f82af62..44691d3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -1,5 +1,5 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
+/*
+ * Copyright (C) 2012 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.
@@ -16,8 +16,11 @@
 
 package com.android.launcher3.appprediction;
 
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.LoggerUtils.newTarget;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -28,6 +31,7 @@
 import android.util.AttributeSet;
 import android.util.IntProperty;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.Interpolator;
 import android.widget.LinearLayout;
@@ -35,17 +39,14 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
@@ -56,6 +57,10 @@
 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
 import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -187,7 +192,7 @@
     public int getExpectedHeight() {
         return getVisibility() == GONE ? 0 :
                 Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
-                + getPaddingTop() + getPaddingBottom();
+                        + getPaddingTop() + getPaddingBottom();
     }
 
     @Override
@@ -237,8 +242,9 @@
             while (getChildCount() > mNumPredictedAppsPerRow) {
                 removeViewAt(0);
             }
+            LayoutInflater inflater = mLauncher.getAppsView().getLayoutInflater();
             while (getChildCount() < mNumPredictedAppsPerRow) {
-                BubbleTextView icon = (BubbleTextView) mLauncher.getLayoutInflater().inflate(
+                BubbleTextView icon = (BubbleTextView) inflater.inflate(
                         R.layout.all_apps_icon, this, false);
                 icon.setOnClickListener(PREDICTION_CLICK_LISTENER);
                 icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
@@ -282,7 +288,8 @@
         mParent.onHeightUpdated();
     }
 
-    private List<ItemInfoWithIcon> processPredictedAppComponents(List<ComponentKeyMapper> components) {
+    private List<ItemInfoWithIcon> processPredictedAppComponents(
+            List<ComponentKeyMapper> components) {
         if (getAppsStore().getApps().length == 0) {
             // Apps have not been bound yet.
             return Collections.emptyList();
@@ -296,7 +303,7 @@
                 predictedApp.container = LauncherSettings.Favorites.CONTAINER_PREDICTION;
                 predictedApps.add(predictedApp);
             } else {
-                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                if (FeatureFlags.IS_STUDIO_BUILD) {
                     Log.e(TAG, "Predicted app not found: " + mapper);
                 }
             }
@@ -309,16 +316,26 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
-            LauncherLogProto.Target targetParent) {
+    public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
+            ArrayList<LauncherLogProto.Target> parents) {
         for (int i = 0; i < mPredictedApps.size(); i++) {
             ItemInfoWithIcon appInfo = mPredictedApps.get(i);
-            if (appInfo == info) {
-                targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
-                target.predictedRank = i;
+            if (appInfo == childInfo) {
+                child.predictedRank = i;
                 break;
             }
         }
+        parents.add(newContainerTarget(LauncherLogProto.ContainerType.PREDICTION));
+
+        // include where the prediction is coming this used to be Launcher#modifyUserEvent
+        LauncherLogProto.Target parent = newTarget(LauncherLogProto.Target.Type.CONTAINER);
+        LauncherState state = mLauncher.getStateManager().getState();
+        if (state == LauncherState.ALL_APPS) {
+            parent.containerType = LauncherLogProto.ContainerType.ALLAPPS;
+        } else if (state == OVERVIEW) {
+            parent.containerType = LauncherLogProto.ContainerType.TASKSWITCHER;
+        }
+        parents.add(parent);
     }
 
     public void setTextAlpha(int textAlpha) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 9c66107..a0f6b04 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,9 +16,11 @@
 
 package com.android.launcher3.appprediction;
 
+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.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
@@ -26,23 +28,23 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -50,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.OptionalInt;
 import java.util.stream.IntStream;
 
 /**
@@ -65,8 +68,8 @@
  * 4) Maintains the current active client id (for the predictions) and all updates are performed on
  * that client id.
  */
-public class PredictionUiStateManager implements StateListener, ItemInfoUpdateReceiver,
-        OnIDPChangeListener, OnUpdateListener {
+public class PredictionUiStateManager implements StateListener<LauncherState>,
+        ItemInfoUpdateReceiver, OnIDPChangeListener, OnUpdateListener {
 
     public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
 
@@ -160,9 +163,6 @@
     public void reapplyItemInfo(ItemInfoWithIcon info) { }
 
     @Override
-    public void onStateTransitionStart(LauncherState toState) { }
-
-    @Override
     public void onStateTransitionComplete(LauncherState state) {
         if (mAppsView == null) {
             return;
@@ -216,8 +216,11 @@
     }
 
     private void dispatchOnChange(boolean changed) {
-        PredictionState newState = changed ? parseLastState() :
-                (mPendingState == null ? mCurrentState : mPendingState);
+        PredictionState newState = changed
+                ? parseLastState()
+                : mPendingState != null && canApplyPredictions(mPendingState)
+                        ? mPendingState
+                        : mCurrentState;
         if (changed && mAppsView != null && !canApplyPredictions(newState)) {
             scheduleApplyPredictedApps(newState);
         } else {
@@ -245,7 +248,7 @@
                     key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
                             appTarget.getClassName()), appTarget.getUser());
                 }
-                state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
+                state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
             }
         }
         updateDependencies(state);
@@ -256,33 +259,8 @@
         if (!state.isEnabled || mAppsView == null) {
             return;
         }
-
-        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
-        List<String> instantAppsToLoad = new ArrayList<>();
-        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
-        int total = state.apps.size();
-        for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
-            ComponentKeyMapper mapper = state.apps.get(i);
-            // Update instant apps
-            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
-                instantAppsToLoad.add(mapper.getPackage());
-                count++;
-            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
-                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
-                count++;
-            } else {
-                // Reload high res icon
-                AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
-                if (info != null) {
-                    if (info.usingLowResIcon()) {
-                        // TODO: Update icon cache to support null callbacks.
-                        iconCache.updateIconInBackground(this, info);
-                    }
-                    count++;
-                }
-            }
-        }
-        mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
+        mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
+                mMaxIconsPerRow);
     }
 
     @Override
@@ -329,6 +307,32 @@
     }
 
     /**
+     * Returns ranking info for the app within all apps prediction.
+     * Only applicable when {@link ItemInfo#itemType} is one of the followings:
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
+     */
+    public OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) {
+        if (itemInfo == null || itemInfo.getTargetComponent() == null || itemInfo.user == null) {
+            return OptionalInt.empty();
+        }
+
+        if (itemInfo.itemType == ITEM_TYPE_APPLICATION
+                || itemInfo.itemType == ITEM_TYPE_SHORTCUT
+                || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
+            ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(),
+                    itemInfo.user);
+            final List<ComponentKeyMapper> apps = getCurrentState().apps;
+            return IntStream.range(0, apps.size())
+                    .filter(index -> key.equals(apps.get(index).getComponentKey()))
+                    .findFirst();
+        }
+
+        return OptionalInt.empty();
+    }
+
+    /**
      * Fill in predicted_rank field based on app prediction.
      * Only applicable when {@link ItemInfo#itemType} is one of the followings:
      * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
@@ -337,6 +341,7 @@
      */
     public static void fillInPredictedRank(
             @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
+
         final PredictionUiStateManager manager = PredictionUiStateManager.INSTANCE.getNoCreate();
         if (manager == null || itemInfo.getTargetComponent() == null || itemInfo.user == null
                 || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
@@ -344,12 +349,17 @@
                 && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
             return;
         }
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) {
+            HotseatPredictionController.encodeHotseatLayoutIntoPredictionRank(itemInfo, target);
+            return;
+        }
+
         final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
         final List<ComponentKeyMapper> predictedApps = manager.getCurrentState().apps;
         IntStream.range(0, predictedApps.size())
                 .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
                 .findFirst()
-                .ifPresent((rank) -> target.predictedRank = rank);
+                .ifPresent((rank) -> target.predictedRank = 0 - rank);
     }
 
     public static class PredictionState {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
new file mode 100644
index 0000000..c968de9
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.hybridhotseat;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ActivityTracker;
+
+/**
+ * Proxy activity to return user to home screen and show halfsheet education
+ */
+public class HotseatEduActivity extends Activity {
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        new HotseatActivityTracker<>().addToIntent(homeIntent);
+        startActivity(homeIntent);
+        finish();
+    }
+
+    static class HotseatActivityTracker<T extends QuickstepLauncher> implements
+            ActivityTracker.SchedulerCallback {
+
+        @Override
+        public boolean init(BaseActivity activity, boolean alreadyOnHome) {
+            QuickstepLauncher launcher = (QuickstepLauncher) activity;
+            if (launcher != null && launcher.getHotseatPredictionController() != null) {
+                launcher.getHotseatPredictionController().showEdu();
+            }
+            return false;
+        }
+
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
new file mode 100644
index 0000000..4f95254
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -0,0 +1,287 @@
+/*
+ * 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 static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+        .LAUNCHER_HOTSEAT_EDU_ONLY_TIP;
+
+import android.content.Intent;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+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.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+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.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.launcher3.views.Snackbar;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * Controller class for managing user onboaridng flow for hybrid hotseat
+ */
+public class HotseatEduController {
+
+    public static final String HOTSEAT_EDU_ACTION =
+            "com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU";
+    public static final String SETTINGS_ACTION =
+            "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
+
+    private final Launcher mLauncher;
+    private final Hotseat mHotseat;
+    private HotseatRestoreHelper mRestoreHelper;
+    private List<WorkspaceItemInfo> mPredictedApps;
+    private HotseatEduDialog mActiveDialog;
+
+    private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
+    private IntArray mNewScreens = null;
+    private Runnable mOnOnboardingComplete;
+
+    HotseatEduController(Launcher launcher, HotseatRestoreHelper restoreHelper, Runnable runnable) {
+        mLauncher = launcher;
+        mHotseat = launcher.getHotseat();
+        mRestoreHelper = restoreHelper;
+        mOnOnboardingComplete = runnable;
+    }
+
+    /**
+     * Checks what type of migration should be used and migrates hotseat
+     */
+    void migrate() {
+        if (mRestoreHelper != null) {
+            mRestoreHelper.createBackup();
+        }
+        if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+            migrateToFolder();
+        } else {
+            migrateHotseatWhole();
+        }
+        Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
+                R.string.hotseat_prediction_settings, null,
+                () -> mLauncher.startActivity(getSettingsIntent()));
+    }
+
+    /**
+     * This migration places all non folder items in the hotseat into a folder and then moves
+     * all folders in the hotseat to a workspace page that has enough empty spots.
+     *
+     * @return pageId that has accepted the items.
+     */
+    private int migrateToFolder() {
+        ArrayDeque<FolderInfo> folders = new ArrayDeque<>();
+        ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
+
+        //separate folders and items that can get in folders
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            View view = mHotseat.getChildAt(i, 0);
+            if (view == null) continue;
+            ItemInfo info = (ItemInfo) view.getTag();
+            if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                folders.add((FolderInfo) info);
+            } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings
+                    .Favorites.CONTAINER_HOTSEAT) {
+                putIntoFolder.add((WorkspaceItemInfo) info);
+            }
+        }
+
+        // create a temp folder and add non folder items to it
+        if (!putIntoFolder.isEmpty()) {
+            ItemInfo firstItem = putIntoFolder.get(0);
+            FolderInfo folderInfo = new FolderInfo();
+            mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
+                    firstItem.screenId, firstItem.cellX, firstItem.cellY);
+            folderInfo.setTitle("", mLauncher.getModelWriter());
+            folderInfo.contents.addAll(putIntoFolder);
+            for (int i = 0; i < folderInfo.contents.size(); i++) {
+                ItemInfo item = folderInfo.contents.get(i);
+                item.rank = i;
+                mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0,
+                        item.cellX, item.cellY);
+            }
+            folders.add(folderInfo);
+        }
+        mNewItems.addAll(folders);
+
+        return placeFoldersInWorkspace(folders);
+    }
+
+    private int placeFoldersInWorkspace(ArrayDeque<FolderInfo> folders) {
+        if (folders.isEmpty()) return 0;
+
+        Workspace workspace = mLauncher.getWorkspace();
+        InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
+
+        GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()];
+        for (int i = 0; i < occupancyList.length; i++) {
+            occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy();
+        }
+        //scan every screen to find available spots to place folders
+        int occupancyIndex = 0;
+        int[] itemXY = new int[2];
+        while (occupancyIndex < occupancyList.length && !folders.isEmpty()) {
+            GridOccupancy occupancy = occupancyList[occupancyIndex];
+            if (occupancy.findVacantCell(itemXY, 1, 1)) {
+                FolderInfo info = folders.poll();
+                mLauncher.getModelWriter().moveItemInDatabase(info,
+                        LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                        workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]);
+                occupancy.markCells(info, true);
+            } else {
+                occupancyIndex++;
+            }
+        }
+        if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex);
+        int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        // if all screens are full and we still have folders left, put those on a new page
+        FolderInfo folderInfo;
+        int col = 0;
+        while ((folderInfo = folders.poll()) != null) {
+            mLauncher.getModelWriter().moveItemInDatabase(folderInfo,
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++,
+                    idp.numRows - 1);
+        }
+        mNewScreens = IntArray.wrap(screenId);
+        return workspace.getPageCount();
+    }
+
+    /**
+     * This migration option attempts to move the entire hotseat up to the first workspace that
+     * has space to host items. If no such page is found, it moves items to a new page.
+     *
+     * @return pageId where items are migrated
+     */
+    private int migrateHotseatWhole() {
+        Workspace workspace = mLauncher.getWorkspace();
+
+        int pageId = -1;
+        int toRow = 0;
+        for (int i = 0; i < workspace.getPageCount(); i++) {
+            CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
+            if (target.makeSpaceForHotseatMigration(true)) {
+                toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
+                pageId = i;
+                break;
+            }
+        }
+        if (pageId == -1) {
+            pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+            mNewScreens = IntArray.wrap(pageId);
+        }
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            View child = mHotseat.getChildAt(i, 0);
+            if (child == null || child.getTag() == null) continue;
+            ItemInfo tag = (ItemInfo) child.getTag();
+            if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue;
+            mLauncher.getModelWriter().moveItemInDatabase(tag,
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
+            mNewItems.add(tag);
+        }
+        return pageId;
+    }
+
+    void moveHotseatItems() {
+        mHotseat.removeAllViewsInLayout();
+        if (!mNewItems.isEmpty()) {
+            int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
+            ArrayList<ItemInfo> animated = new ArrayList<>();
+            ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
+
+            for (ItemInfo info : mNewItems) {
+                if (info.screenId == lastPage) {
+                    animated.add(info);
+                } else {
+                    nonAnimated.add(info);
+                }
+            }
+            mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
+        }
+    }
+
+    void finishOnboarding() {
+        mOnOnboardingComplete.run();
+    }
+
+    void showDimissTip() {
+        if (mHotseat.getShortcutsAndWidgets().getChildCount()
+                < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
+            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+                    R.string.hotseat_prediction_settings, null,
+                    () -> mLauncher.startActivity(getSettingsIntent()));
+        } else {
+            new ArrowTipView(mLauncher).show(
+                    mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
+        }
+    }
+
+    void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
+        mPredictedApps = predictedApps;
+    }
+
+    void showEdu() {
+        int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
+        CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);
+        // hotseat is already empty and does not require migration. show edu tip
+        boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> {
+            View v = mHotseat.getShortcutsAndWidgets().getChildAt(i);
+            return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container
+                    != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+        });
+        boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false);
+        if (requiresMigration && canMigrateToFirstPage) {
+            showDialog();
+        } else {
+            new ArrowTipView(mLauncher).show(mLauncher.getString(
+                    requiresMigration ? R.string.hotseat_tip_no_empty_slots
+                            : R.string.hotseat_auto_enrolled),
+                    mHotseat.getTop());
+            mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ONLY_TIP);
+            finishOnboarding();
+        }
+    }
+
+    void showDialog() {
+        if (mPredictedApps == null || mPredictedApps.isEmpty()) {
+            return;
+        }
+        if (mActiveDialog != null) {
+            mActiveDialog.handleClose(false);
+        }
+        mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
+        mActiveDialog.setHotseatEduController(this);
+        mActiveDialog.show(mPredictedApps);
+    }
+
+    static Intent getSettingsIntent() {
+        return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+}
+
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
new file mode 100644
index 0000000..4b8e434
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -0,0 +1,229 @@
+/*
+ * 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 static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+        .LAUNCHER_HOTSEAT_EDU_ACCEPT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.views.AbstractSlideInView;
+
+import java.util.List;
+
+/**
+ * User education dialog for hybrid hotseat. Allows user to migrate hotseat items to a new page in
+ * the workspace and shows predictions on the whole hotseat
+ */
+public class HotseatEduDialog extends AbstractSlideInView implements Insettable {
+
+    private static final int DEFAULT_CLOSE_DURATION = 200;
+    protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
+
+    // we use this value to keep track of migration logs as we experiment with different migrations
+    private static final int MIGRATION_EXPERIMENT_IDENTIFIER = 1;
+
+    private final Rect mInsets = new Rect();
+    private View mHotseatWrapper;
+    private CellLayout mSampleHotseat;
+    private Button mDismissBtn;
+
+    public void setHotseatEduController(HotseatEduController hotseatEduController) {
+        mHotseatEduController = hotseatEduController;
+    }
+
+    private HotseatEduController mHotseatEduController;
+
+    public HotseatEduDialog(Context context, AttributeSet attr) {
+        this(context, attr, 0);
+    }
+
+    public HotseatEduDialog(Context context, AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContent = this;
+    }
+
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
+        mSampleHotseat = findViewById(R.id.sample_prediction);
+
+        DeviceProfile grid = mLauncher.getDeviceProfile();
+        Rect padding = grid.getHotseatLayoutPadding();
+
+        mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
+        mSampleHotseat.setGridSize(grid.inv.numHotseatIcons, 1);
+        mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
+
+        Button turnOnBtn = findViewById(R.id.turn_predictions_on);
+        turnOnBtn.setOnClickListener(this::onAccept);
+
+        mDismissBtn = findViewById(R.id.no_thanks);
+        mDismissBtn.setOnClickListener(this::onDismiss);
+
+        // update ui to reflect which migration method is going to be used
+        if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+            ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
+                    R.string.hotseat_edu_message_migrate_alt);
+        }
+    }
+
+    private void onAccept(View v) {
+        mHotseatEduController.migrate();
+        handleClose(true);
+
+        mHotseatEduController.moveHotseatItems();
+        mHotseatEduController.finishOnboarding();
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ACCEPT);
+    }
+
+    private void onDismiss(View v) {
+        mHotseatEduController.showDimissTip();
+        mHotseatEduController.finishOnboarding();
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_DENY);
+        handleClose(true);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // Since this is on-boarding popup, it is not a user controlled action.
+    }
+
+    @Override
+    public int getLogContainerType() {
+        return LauncherLogProto.ContainerType.TIP;
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        int leftInset = insets.left - mInsets.left;
+        int rightInset = insets.right - mInsets.right;
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        if (mLauncher.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+            setPadding(leftInset, getPaddingTop(), rightInset, 0);
+            mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+                    mHotseatWrapper.getPaddingRight(), bottomInset);
+            mHotseatWrapper.getLayoutParams().height =
+                    mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
+
+        } else {
+            setPadding(0, getPaddingTop(), 0, 0);
+            mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+                    mHotseatWrapper.getPaddingRight(),
+                    (int) getResources().getDimension(R.dimen.bottom_sheet_edu_padding));
+            ((TextView) findViewById(R.id.hotseat_edu_heading)).setText(
+                    R.string.hotseat_edu_title_migrate_landscape);
+            ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
+                    R.string.hotseat_edu_message_migrate_landscape);
+        }
+    }
+
+    private void animateOpen() {
+        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+            return;
+        }
+        mIsOpen = true;
+        mOpenCloseAnimator.setValues(
+                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mOpenCloseAnimator.start();
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        handleClose(true, DEFAULT_CLOSE_DURATION);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        handleClose(false);
+    }
+
+    @Override
+    protected int getScrimColor(Context context) {
+        return FINAL_SCRIM_BG_COLOR;
+    }
+
+    private void populatePreview(List<WorkspaceItemInfo> predictions) {
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            WorkspaceItemInfo info = predictions.get(i);
+            PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
+            icon.setEnabled(false);
+            icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+            icon.verifyHighRes();
+            CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1);
+            mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
+        }
+    }
+
+    /**
+     * Opens User education dialog with a list of suggested apps
+     */
+    public void show(List<WorkspaceItemInfo> predictions) {
+        if (getParent() != null
+                || predictions.size() < mLauncher.getDeviceProfile().inv.numHotseatIcons
+                || mHotseatEduController == null) {
+            return;
+        }
+        AbstractFloatingView.closeAllOpenViews(mLauncher);
+        attachToContainer();
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_SEEN);
+        animateOpen();
+        populatePreview(predictions);
+    }
+
+    /**
+     * Factory method for HotseatPredictionUserEdu dialog
+     */
+    public static HotseatEduDialog getDialog(Launcher launcher) {
+        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+        return (HotseatEduDialog) layoutInflater.inflate(
+                R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
+                false);
+
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
new file mode 100644
index 0000000..c15a596
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
@@ -0,0 +1,129 @@
+/*
+ * 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() {
+        String fName = FILE_NAME_PREFIX + (LOG_DAYS % 10);
+        if (fName.equals(mFileName)) return mCurrentWriter;
+
+        Calendar cal = Calendar.getInstance();
+
+        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/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
new file mode 100644
index 0000000..b94e633
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.content.ComponentName;
+import android.os.Process;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.appprediction.ComponentKeyMapper;
+import com.android.launcher3.appprediction.DynamicItemCache;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer;
+import com.android.launcher3.logging.InstanceId;
+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.ItemInfoWithIcon;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.launcher3.views.Snackbar;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
+
+/**
+ * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
+ * pinning of predicted apps and manages replacement of predicted apps with user drag.
+ */
+public class HotseatPredictionController implements DragController.DragListener,
+        View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
+        InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
+        IconCache.ItemInfoUpdateReceiver, DragSource {
+
+    private static final String TAG = "PredictiveHotseat";
+    private static final boolean DEBUG = false;
+
+    private static final String PREDICTION_CLIENT = "hotseat";
+    private DropTarget.DragObject mDragObject;
+    private int mHotSeatItemsCount;
+    private int mPredictedSpotsCount = 0;
+
+    private Launcher mLauncher;
+    private final Hotseat mHotseat;
+
+    private final HotseatRestoreHelper mRestoreHelper;
+
+    private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
+
+    private DynamicItemCache mDynamicItemCache;
+
+    private final HotseatPredictionModel mPredictionModel;
+    private AppPredictor mAppPredictor;
+    private AllAppsStore mAllAppsStore;
+    private AnimatorSet mIconRemoveAnimators;
+    private boolean mUIUpdatePaused = false;
+    private boolean mIsDestroyed = false;
+
+
+    private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
+
+    private final View.OnLongClickListener mPredictionLongClickListener = v -> {
+        if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+        if (mLauncher.getWorkspace().isSwitchingState()) return false;
+        if (!mLauncher.getOnboardingPrefs().getBoolean(
+                OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) {
+            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+                    R.string.hotseat_prediction_settings, null,
+                    () -> mLauncher.startActivity(getSettingsIntent()));
+            mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN);
+            mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            return true;
+        }
+        // Start the drag
+        mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
+        return true;
+    };
+
+    public HotseatPredictionController(Launcher launcher) {
+        mLauncher = launcher;
+        mHotseat = launcher.getHotseat();
+        mAllAppsStore = mLauncher.getAppsView().getAppsStore();
+        LauncherAppState appState = LauncherAppState.getInstance(launcher);
+        mPredictionModel = (HotseatPredictionModel) appState.getPredictionModel();
+        mAllAppsStore.addUpdateListener(this);
+        mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
+        mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
+        launcher.getDeviceProfile().inv.addOnChangeListener(this);
+        mHotseat.addOnAttachStateChangeListener(this);
+        mRestoreHelper = new HotseatRestoreHelper(mLauncher);
+        if (mHotseat.isAttachedToWindow()) {
+            onViewAttachedToWindow(mHotseat);
+        }
+    }
+
+    /**
+     * Shows appropriate hotseat education based on prediction enabled and migration states.
+     */
+    public void showEdu() {
+        mLauncher.getStateManager().goToState(NORMAL, true, () -> {
+            if (mComponentKeyMappers.isEmpty()) {
+                // launcher has empty predictions set
+                Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
+                        R.string.hotseat_prediction_settings, null,
+                        () -> mLauncher.startActivity(getSettingsIntent()));
+            } else if (getPredictedIcons().size() >= (mHotSeatItemsCount + 1) / 2) {
+                showDiscoveryTip();
+            } else {
+                HotseatEduController eduController = new HotseatEduController(mLauncher,
+                        mRestoreHelper,
+                        this::createPredictor);
+                eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+                eduController.showEdu();
+            }
+        });
+    }
+
+    /**
+     * Shows educational tip for hotseat if user does not go through Tips app.
+     */
+    private void showDiscoveryTip() {
+        if (getPredictedIcons().isEmpty()) {
+            new ArrowTipView(mLauncher).show(
+                    mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
+        } else {
+            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+                    R.string.hotseat_prediction_settings, null,
+                    () -> mLauncher.startActivity(getSettingsIntent()));
+        }
+    }
+
+    /**
+     * Returns if hotseat client has predictions
+     */
+    public boolean hasPredictions() {
+        return !mComponentKeyMappers.isEmpty();
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View view) {
+        mLauncher.getDragController().addDragListener(this);
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        mLauncher.getDragController().removeDragListener(this);
+    }
+
+    private void fillGapsWithPrediction() {
+        fillGapsWithPrediction(false, null);
+    }
+
+    private void fillGapsWithPrediction(boolean animate, Runnable callback) {
+        if (mUIUpdatePaused || mDragObject != null) {
+            return;
+        }
+        List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
+        if (mComponentKeyMappers.isEmpty() != predictedApps.isEmpty()) {
+            // Safely ignore update as AppsList is not ready yet. This will called again once
+            // apps are ready (HotseatPredictionController#onAppsUpdated)
+            return;
+        }
+        int predictionIndex = 0;
+        ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
+        // make sure predicted icon removal and filling predictions don't step on each other
+        if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
+            mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    fillGapsWithPrediction(animate, callback);
+                    mIconRemoveAnimators.removeListener(this);
+                }
+            });
+            return;
+        }
+        for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
+            View child = mHotseat.getChildAt(
+                    mHotseat.getCellXFromOrder(rank),
+                    mHotseat.getCellYFromOrder(rank));
+
+            if (child != null && !isPredictedIcon(child)) {
+                continue;
+            }
+            if (predictedApps.size() <= predictionIndex) {
+                // Remove predicted apps from the past
+                if (isPredictedIcon(child)) {
+                    mHotseat.removeView(child);
+                }
+                continue;
+            }
+            WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
+            if (isPredictedIcon(child) && child.isEnabled()) {
+                PredictedAppIcon icon = (PredictedAppIcon) child;
+                icon.applyFromWorkspaceItem(predictedItem);
+                icon.finishBinding(mPredictionLongClickListener);
+            } else {
+                newItems.add(predictedItem);
+            }
+            preparePredictionInfo(predictedItem, rank);
+        }
+        mPredictedSpotsCount = predictionIndex;
+        bindItems(newItems, animate, callback);
+    }
+
+    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
+        AnimatorSet animationSet = new AnimatorSet();
+        for (WorkspaceItemInfo item : itemsToAdd) {
+            PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
+            mLauncher.getWorkspace().addInScreenFromBind(icon, item);
+            icon.finishBinding(mPredictionLongClickListener);
+            if (animate) {
+                animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
+            }
+        }
+        if (animate) {
+            if (callback != null) {
+                animationSet.addListener(AnimationSuccessListener.forRunnable(callback));
+            }
+            animationSet.start();
+        } else {
+            if (callback != null) callback.run();
+        }
+    }
+
+    /**
+     * Unregisters callbacks and frees resources
+     */
+    public void destroy() {
+        mIsDestroyed = true;
+        mAllAppsStore.removeUpdateListener(this);
+        mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
+        mHotseat.removeOnAttachStateChangeListener(this);
+        if (mAppPredictor != null) {
+            mAppPredictor.destroy();
+        }
+    }
+
+    /**
+     * start and pauses predicted apps update on the hotseat
+     */
+    public void setPauseUIUpdate(boolean paused) {
+        mUIUpdatePaused = paused;
+        if (!paused) {
+            fillGapsWithPrediction();
+        }
+    }
+
+    /**
+     * Creates App Predictor with all the current apps pinned on the hotseat
+     */
+    public void createPredictor() {
+        AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+        if (mAppPredictor != null) {
+            mAppPredictor.destroy();
+            mAppPredictor = null;
+        }
+        WeakReference<HotseatPredictionController> controllerRef = new WeakReference<>(this);
+
+
+        mPredictionModel.createBundle(bundle -> {
+            if (mIsDestroyed) return;
+            mAppPredictor = apm.createAppPredictionSession(
+                    new AppPredictionContext.Builder(mLauncher)
+                            .setUiSurface(PREDICTION_CLIENT)
+                            .setPredictedTargetCount(mHotSeatItemsCount)
+                            .setExtras(bundle)
+                            .build());
+            mAppPredictor.registerPredictionUpdates(
+                    mLauncher.getApplicationContext().getMainExecutor(),
+                    list -> {
+                        if (controllerRef.get() != null) {
+                            controllerRef.get().setPredictedApps(list);
+                        }
+                    });
+            mAppPredictor.requestPredictionUpdate();
+        });
+        setPauseUIUpdate(false);
+    }
+
+    /**
+     * Create WorkspaceItemInfo objects and binds PredictedAppIcon views for cached predicted items.
+     */
+    public void showCachedItems(List<AppInfo> apps, IntArray ranks) {
+        if (hasPredictions() && mAppPredictor != null) {
+            mAppPredictor.requestPredictionUpdate();
+            fillGapsWithPrediction();
+            return;
+        }
+        int count = Math.min(ranks.size(), apps.size());
+        List<WorkspaceItemInfo> items = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            WorkspaceItemInfo item = new WorkspaceItemInfo(apps.get(i));
+            ComponentKey componentKey = new ComponentKey(item.getTargetComponent(), item.user);
+            preparePredictionInfo(item, ranks.get(i));
+            items.add(item);
+
+            mComponentKeyMappers.add(new ComponentKeyMapper(componentKey, mDynamicItemCache));
+        }
+        updateDependencies();
+        bindItems(items, false, null);
+    }
+
+    private void setPredictedApps(List<AppTarget> appTargets) {
+        mComponentKeyMappers.clear();
+        if (appTargets.isEmpty()) {
+            mRestoreHelper.restoreBackup();
+        }
+        StringBuilder predictionLog = new StringBuilder("predictedApps: [\n");
+        ArrayList<ComponentKey> componentKeys = new ArrayList<>();
+        for (AppTarget appTarget : appTargets) {
+            ComponentKey key;
+            if (appTarget.getShortcutInfo() != null) {
+                key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
+            } else {
+                key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
+                        appTarget.getClassName()), appTarget.getUser());
+            }
+            componentKeys.add(key);
+            predictionLog.append(key.toString());
+            predictionLog.append(",rank:");
+            predictionLog.append(appTarget.getRank());
+            predictionLog.append("\n");
+            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
+        }
+        predictionLog.append("]");
+        if (Utilities.IS_DEBUG_DEVICE) {
+            HotseatFileLog.INSTANCE.get(mLauncher).log(TAG, predictionLog.toString());
+        }
+        updateDependencies();
+        fillGapsWithPrediction();
+        mPredictionModel.cachePredictionComponentKeys(componentKeys);
+    }
+
+    private void updateDependencies() {
+        mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this,
+                mHotSeatItemsCount);
+    }
+
+    /**
+     * Pins a predicted app icon into place.
+     */
+    public void pinPrediction(ItemInfo info) {
+        PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
+                mHotseat.getCellXFromOrder(info.rank),
+                mHotseat.getCellYFromOrder(info.rank));
+        if (icon == null) {
+            return;
+        }
+        WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+        mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
+                LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
+                workspaceItemInfo.cellX, workspaceItemInfo.cellY);
+        ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
+        icon.pin(workspaceItemInfo);
+        AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(workspaceItemInfo);
+        if (appTarget != null) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                    AppTargetEvent.ACTION_PIN, workspaceItemInfo));
+        }
+    }
+
+    private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
+            List<ComponentKeyMapper> components) {
+        AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
+        if (allAppsStore.getApps().length == 0) {
+            return Collections.emptyList();
+        }
+
+        List<WorkspaceItemInfo> predictedApps = new ArrayList<>();
+        for (ComponentKeyMapper mapper : components) {
+            ItemInfoWithIcon info = mapper.getApp(allAppsStore);
+            if (info instanceof AppInfo) {
+                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
+                predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+                predictedApps.add(predictedApp);
+            } else if (info instanceof WorkspaceItemInfo) {
+                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+                predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+                predictedApps.add(predictedApp);
+            } else {
+                if (DEBUG) {
+                    Log.e(TAG, "Predicted app not found: " + mapper);
+                }
+            }
+            // Stop at the number of hotseat items
+            if (predictedApps.size() == mHotSeatItemsCount) {
+                break;
+            }
+        }
+        return predictedApps;
+    }
+
+    private List<PredictedAppIcon> getPredictedIcons() {
+        List<PredictedAppIcon> icons = new ArrayList<>();
+        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
+        for (int i = 0; i < vg.getChildCount(); i++) {
+            View child = vg.getChildAt(i);
+            if (isPredictedIcon(child)) {
+                icons.add((PredictedAppIcon) child);
+            }
+        }
+        return icons;
+    }
+
+    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
+            ItemInfo draggedInfo) {
+        if (mIconRemoveAnimators != null) {
+            mIconRemoveAnimators.end();
+        }
+        mIconRemoveAnimators = new AnimatorSet();
+        removeOutlineDrawings();
+        for (PredictedAppIcon icon : getPredictedIcons()) {
+            if (!icon.isEnabled()) {
+                continue;
+            }
+            if (icon.getTag().equals(draggedInfo)) {
+                mHotseat.removeView(icon);
+                continue;
+            }
+            int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
+            outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
+                    mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
+            icon.setEnabled(false);
+            ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
+            animator.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (icon.getParent() != null) {
+                        mHotseat.removeView(icon);
+                    }
+                }
+            });
+            mIconRemoveAnimators.play(animator);
+        }
+        mIconRemoveAnimators.start();
+    }
+
+    private void notifyItemAction(AppTargetEvent event) {
+        if (mAppPredictor != null) {
+            mAppPredictor.notifyAppTargetEvent(event);
+        }
+    }
+
+    @Override
+    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+        removePredictedApps(mOutlineDrawings, dragObject.dragInfo);
+        mDragObject = dragObject;
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.addDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
+    }
+
+    /**
+     * Unpins pinned app when it's converted into a folder
+     */
+    public void folderCreatedFromWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
+        AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
+        AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
+        if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
+                    AppTargetEvent.ACTION_PIN, folderInfo));
+        }
+        // using folder info with isTrackedForPrediction as itemInfo.container is already changed
+        // to folder by this point
+        if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
+                    AppTargetEvent.ACTION_UNPIN, folderInfo
+            ));
+        }
+    }
+
+    /**
+     * Pins workspace item created when all folder items are removed but one
+     */
+    public void folderConvertedToWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
+        AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
+        AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
+        if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
+                    AppTargetEvent.ACTION_UNPIN, folderInfo));
+        }
+        if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(itemInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
+                    AppTargetEvent.ACTION_PIN, itemInfo));
+        }
+    }
+
+    @Override
+    public void onDragEnd() {
+        if (mDragObject == null) {
+            return;
+        }
+
+        ItemInfo dragInfo = mDragObject.dragInfo;
+        if (mDragObject.isMoved()) {
+            AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(dragInfo);
+            //always send pin event first to prevent AiAi from predicting an item moved within
+            // the same page
+            if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(dragInfo)) {
+                notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                        AppTargetEvent.ACTION_PIN, dragInfo));
+            }
+            if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(
+                    mDragObject.originalDragInfo)) {
+                notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                        AppTargetEvent.ACTION_UNPIN, mDragObject.originalDragInfo));
+            }
+        }
+        mDragObject = null;
+        fillGapsWithPrediction(true, this::removeOutlineDrawings);
+    }
+
+
+    @Nullable
+    @Override
+    public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
+            ItemInfo itemInfo) {
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            return null;
+        }
+        return new PinPrediction(activity, itemInfo);
+    }
+
+    private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
+        itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+        itemInfo.rank = rank;
+        itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+        itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
+        itemInfo.screenId = rank;
+    }
+
+    private void removeOutlineDrawings() {
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
+        mOutlineDrawings.clear();
+    }
+
+    @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+            this.mHotSeatItemsCount = profile.numHotseatIcons;
+            createPredictor();
+        }
+    }
+
+    @Override
+    public void onAppsUpdated() {
+        fillGapsWithPrediction();
+    }
+
+    @Override
+    public void reapplyItemInfo(ItemInfoWithIcon info) {
+    }
+
+    @Override
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
+        //Does nothing
+    }
+
+    @Override
+    public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
+            ArrayList<LauncherLogProto.Target> parents) {
+        mHotseat.fillInLogContainerData(childInfo, child, parents);
+    }
+
+    /**
+     * 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);
+        }
+
+        if (itemInfo.getTargetComponent() == null || itemInfo.user == null) {
+            return;
+        }
+
+        final ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
+
+        final List<ComponentKeyMapper> predictedApps = new ArrayList<>(mComponentKeyMappers);
+        OptionalInt rank = IntStream.range(0, predictedApps.size())
+                .filter(index -> key.equals(predictedApps.get(index).getComponentKey()))
+                .findFirst();
+        if (!rank.isPresent()) {
+            return;
+        }
+
+        int cardinality = 0;
+        for (PredictedAppIcon icon : getPredictedIcons()) {
+            ItemInfo info = (ItemInfo) icon.getTag();
+            cardinality |= 1 << info.screenId;
+        }
+
+        PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder();
+        containerBuilder.setCardinality(cardinality);
+        if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            containerBuilder.setIndex(rank.getAsInt());
+        }
+        mLauncher.getStatsLogManager().logger()
+                .withInstanceId(instanceId)
+                .withRank(rank.getAsInt())
+                .withContainerInfo(ContainerInfo.newBuilder()
+                        .setPredictedHotseatContainer(containerBuilder)
+                        .build())
+                .log(LAUNCHER_HOTSEAT_RANKED);
+    }
+
+    private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
+
+        private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
+            super(R.drawable.ic_pin, R.string.pin_prediction, target,
+                    itemInfo);
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            pinPrediction(mItemInfo);
+        }
+    }
+
+    /**
+     * Fill in predicted_rank field based on app prediction.
+     * Only applicable when {@link ItemInfo#itemType} is PREDICTED_HOTSEAT
+     */
+    public static void encodeHotseatLayoutIntoPredictionRank(
+            @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
+        QuickstepLauncher launcher = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+        if (launcher == null || launcher.getHotseatPredictionController() == null
+                || itemInfo.getTargetComponent() == null) {
+            return;
+        }
+        HotseatPredictionController controller = launcher.getHotseatPredictionController();
+
+        final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
+
+        final List<ComponentKeyMapper> predictedApps = controller.mComponentKeyMappers;
+        OptionalInt rank = IntStream.range(0, predictedApps.size())
+                .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
+                .findFirst();
+
+        target.predictedRank = 10000 + (controller.mPredictedSpotsCount * 100)
+                + (rank.isPresent() ? rank.getAsInt() + 1 : 0);
+    }
+
+    private static boolean isPredictedIcon(View view) {
+        return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
+                && ((WorkspaceItemInfo) view.getTag()).container
+                == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
new file mode 100644
index 0000000..5a038d2
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -0,0 +1,136 @@
+/*
+ * 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 static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.PredictionModel;
+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.shortcuts.ShortcutKey;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+/**
+ * Model helper for app predictions in workspace
+ */
+public class HotseatPredictionModel extends PredictionModel {
+    private static final String APP_LOCATION_HOTSEAT = "hotseat";
+    private static final String APP_LOCATION_WORKSPACE = "workspace";
+
+    private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
+    private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items";
+
+
+    public HotseatPredictionModel(Context context) { }
+
+    /**
+     * Creates and returns bundle using workspace items and cached items
+     */
+    public void createBundle(Consumer<Bundle> cb) {
+        LauncherAppState appState = LauncherAppState.getInstance(mContext);
+        appState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+            @Override
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                Bundle bundle = new Bundle();
+                ArrayList<AppTargetEvent> events = new ArrayList<>();
+                ArrayList<ItemInfo> workspaceItems = new ArrayList<>(dataModel.workspaceItems);
+                workspaceItems.addAll(dataModel.appWidgets);
+                for (ItemInfo item : workspaceItems) {
+                    AppTarget target = getAppTargetFromInfo(item);
+                    if (target != null && !isTrackedForPrediction(item)) continue;
+                    events.add(wrapAppTargetWithLocation(target, AppTargetEvent.ACTION_PIN, item));
+                }
+                ArrayList<AppTarget> currentTargets = new ArrayList<>();
+                for (ItemInfo itemInfo : dataModel.cachedPredictedItems) {
+                    AppTarget target = getAppTargetFromInfo(itemInfo);
+                    if (target != null) currentTargets.add(target);
+                }
+                bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events);
+                bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets);
+                MAIN_EXECUTOR.execute(() -> cb.accept(bundle));
+            }
+        });
+    }
+
+    /**
+     * Creates and returns for {@link AppTarget} object given an {@link ItemInfo}. Returns null
+     * if item is not supported prediction
+     */
+    public AppTarget getAppTargetFromInfo(ItemInfo info) {
+        if (info == null) return null;
+        if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+                && info instanceof LauncherAppWidgetInfo
+                && ((LauncherAppWidgetInfo) info).providerName != null) {
+            ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
+            return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                && info.getTargetComponent() != null) {
+            ComponentName cn = info.getTargetComponent();
+            return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                && info instanceof WorkspaceItemInfo) {
+            ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
+            //TODO: switch to using full shortcut info
+            return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
+                    shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
+                    mContext.getPackageName(), info.user).build();
+        }
+        return null;
+    }
+
+
+    /**
+     * Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item
+     * location using {@link ItemInfo}
+     */
+    public AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, ItemInfo info) {
+        String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]",
+                info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                        ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE,
+                info.screenId, info.cellX, info.cellY, info.spanX, info.spanY);
+        return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build();
+    }
+
+    /**
+     * Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
+     */
+    public static boolean isTrackedForPrediction(ItemInfo info) {
+        return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || (
+                info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
+                        && info.screenId == Workspace.FIRST_SCREEN_ID);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
new file mode 100644
index 0000000..8c1db4e
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -0,0 +1,86 @@
+/*
+ * 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 static com.android.launcher3.LauncherSettings.Favorites.HYBRID_HOTSEAT_BACKUP_TABLE;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.GridBackupTable;
+import com.android.launcher3.provider.LauncherDbUtils;
+
+/**
+ * A helper class to manage migration revert restoration for hybrid hotseat
+ */
+public class HotseatRestoreHelper {
+    private final Launcher mLauncher;
+    private boolean mBackupRestored = false;
+
+    HotseatRestoreHelper(Launcher context) {
+        mLauncher = context;
+    }
+
+    /**
+     * Creates a snapshot backup of Favorite table for future restoration use.
+     */
+    public void createBackup() {
+        MODEL_EXECUTOR.execute(() -> {
+            try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
+                    LauncherSettings.Settings.call(
+                            mLauncher.getContentResolver(),
+                            LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
+                            .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+                InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
+                GridBackupTable backupTable = new GridBackupTable(mLauncher,
+                        transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
+                        idp.numRows);
+                backupTable.createCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE);
+                transaction.commit();
+                LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+                        LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE);
+            }
+        });
+    }
+
+    /**
+     * Finds and restores a previously saved snapshow of Favorites table
+     */
+    public void restoreBackup() {
+        if (mBackupRestored) return;
+        MODEL_EXECUTOR.execute(() -> {
+            try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
+                    LauncherSettings.Settings.call(
+                            mLauncher.getContentResolver(),
+                            LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
+                            .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+                if (!tableExists(transaction.getDb(), HYBRID_HOTSEAT_BACKUP_TABLE)) {
+                    return;
+                }
+                InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
+                GridBackupTable backupTable = new GridBackupTable(mLauncher,
+                        transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
+                        idp.numRows);
+                backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
+                transaction.commit();
+                mBackupRestored = true;
+                mLauncher.getModel().forceReload();
+            }
+        });
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
new file mode 100644
index 0000000..597c17b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION;
+import static com.android.launcher3.graphics.IconShape.getShape;
+
+import android.content.Context;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.os.Process;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.CellLayout;
+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;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.DoubleShadowBubbleTextView;
+
+/**
+ * A BubbleTextView with a ring around it's drawable
+ */
+public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
+        LauncherAccessibilityDelegate.AccessibilityActionHandler {
+
+    private static final int RING_SHADOW_COLOR = 0x99000000;
+    private static final float RING_EFFECT_RATIO = 0.095f;
+
+    boolean mIsDrawingDot = false;
+    private final DeviceProfile mDeviceProfile;
+    private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Path mRingPath = new Path();
+    private boolean mIsPinned = false;
+    private final int mNormalizedIconRadius;
+    private final BlurMaskFilter mShadowFilter;
+    private int mPlateColor;
+    boolean mDrawForDrag = false;
+
+    public PredictedAppIcon(Context context) {
+        this(context, null, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
+        mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
+        int shadowSize = context.getResources().getDimensionPixelSize(
+                R.dimen.blur_size_thin_outline);
+        mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        int count = canvas.save();
+        if (!mIsPinned) {
+            boolean isBadged = getTag() instanceof WorkspaceItemInfo
+                    && !Process.myUserHandle().equals(((ItemInfo) getTag()).user);
+            drawEffect(canvas, isBadged);
+            canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
+            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+        }
+        super.onDraw(canvas);
+        canvas.restoreToCount(count);
+    }
+
+    @Override
+    protected void drawDotIfNecessary(Canvas canvas) {
+        mIsDrawingDot = true;
+        int count = canvas.save();
+        canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
+        canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
+        super.drawDotIfNecessary(canvas);
+        canvas.restoreToCount(count);
+        mIsDrawingDot = false;
+    }
+
+    @Override
+    public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
+        super.applyFromWorkspaceItem(info);
+        int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
+        mPlateColor = ColorUtils.setAlphaComponent(color, 200);
+        if (mIsPinned) {
+            setContentDescription(info.contentDescription);
+        } else {
+            setContentDescription(
+                    getContext().getString(R.string.hotseat_prediction_content_description,
+                            info.contentDescription));
+        }
+    }
+
+    /**
+     * Removes prediction ring from app icon
+     */
+    public void pin(WorkspaceItemInfo info) {
+        if (mIsPinned) return;
+        mIsPinned = true;
+        applyFromWorkspaceItem(info);
+        setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
+        invalidate();
+    }
+
+    /**
+     * prepares prediction icon for usage after bind
+     */
+    public void finishBinding(OnLongClickListener longClickListener) {
+        setOnLongClickListener(longClickListener);
+        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
+        setTextVisibility(false);
+        verifyHighRes();
+    }
+
+    @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) {
+            int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
+            outBounds.inset(predictionInset, predictionInset);
+        }
+    }
+
+    private int getOutlineOffsetX() {
+        return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
+    }
+
+    private int getOutlineOffsetY() {
+        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+    }
+
+    private void drawEffect(Canvas canvas, boolean isBadged) {
+        // Don't draw ring effect if item is about to be dragged.
+        if (mDrawForDrag) {
+            return;
+        }
+        mRingPath.reset();
+        getShape().addToPath(mRingPath, getOutlineOffsetX(), getOutlineOffsetY(),
+                mNormalizedIconRadius);
+        if (isBadged) {
+            float outlineSize = mNormalizedIconRadius * RING_EFFECT_RATIO * 2;
+            float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
+            float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
+            float badgeInset = mNormalizedIconRadius * 2 - badgeSize;
+            getShape().addToPath(mRingPath, getOutlineOffsetX() + badgeInset,
+                    getOutlineOffsetY() + badgeInset, badgeSize / 2);
+
+        }
+        mIconRingPaint.setColor(RING_SHADOW_COLOR);
+        mIconRingPaint.setMaskFilter(mShadowFilter);
+        canvas.drawPath(mRingPath, mIconRingPaint);
+        mIconRingPaint.setColor(mPlateColor);
+        mIconRingPaint.setMaskFilter(null);
+        canvas.drawPath(mRingPath, mIconRingPaint);
+    }
+
+    @Override
+    public void getSourceVisualDragBounds(Rect bounds) {
+        super.getSourceVisualDragBounds(bounds);
+        if (!mIsPinned) {
+            int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
+            bounds.inset(internalSize, internalSize);
+        }
+    }
+
+    @Override
+    public SafeCloseable prepareDrawDragView() {
+        mDrawForDrag = true;
+        invalidate();
+        SafeCloseable r = super.prepareDrawDragView();
+        return () -> {
+            r.close();
+            mDrawForDrag = false;
+        };
+    }
+
+    /**
+     * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
+     */
+    public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
+        PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.predicted_app_icon, parent, false);
+        icon.applyFromWorkspaceItem(info);
+        icon.setOnClickListener(ItemClickHandler.INSTANCE);
+        icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
+        return icon;
+    }
+
+    /**
+     * Draws Predicted Icon outline on cell layout
+     */
+    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);
+
+        public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
+            mDelegateCellX = cellX;
+            mDelegateCellY = cellY;
+            mOffsetX = icon.getOutlineOffsetX();
+            mOffsetY = icon.getOutlineOffsetY();
+            mIconRadius = icon.mNormalizedIconRadius;
+            mOutlinePaint.setStyle(Paint.Style.FILL);
+            mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
+        }
+
+        /**
+         * Draws predicted app icon outline under CellLayout
+         */
+        @Override
+        public void drawUnderItem(Canvas canvas) {
+            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+        }
+
+        /**
+         * Draws PredictedAppIcon outline over CellLayout
+         */
+        @Override
+        public void drawOverItem(Canvas canvas) {
+            // Does nothing
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
new file mode 100644
index 0000000..8f1d319
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+/** A util class that inflates a predicted app icon */
+public class PredictedAppIconInflater {
+    public static View inflate(LayoutInflater inflater, ViewGroup parent, WorkspaceItemInfo info) {
+        PredictedAppIcon icon = (PredictedAppIcon) inflater.inflate(
+                R.layout.predicted_app_icon, parent, false);
+        icon.applyFromWorkspaceItem(info);
+        return icon;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
new file mode 100644
index 0000000..9a03d92
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
+import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
+import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
+import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.Stream;
+
+public class QuickstepLauncher extends BaseQuickstepLauncher {
+
+    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
+    /**
+     * Reusable command for applying the shelf height on the background thread.
+     */
+    public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
+            SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.createPredictor();
+        }
+    }
+
+    @Override
+    protected void setupViews() {
+        super.setupViews();
+        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+            mHotseatPredictionController = new HotseatPredictionController(this);
+        }
+    }
+
+    @Override
+    protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
+        StatsLogger logger = getStatsLogManager()
+                .logger().withItemInfo(info).withInstanceId(instanceId);
+        OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
+        allAppsRank.ifPresent(logger::withRank);
+        logger.log(LAUNCHER_APP_LAUNCH_TAP);
+
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        onStateOrResumeChanging(false /* inTransition */);
+    }
+
+    @Override
+    public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
+            @Nullable String sourceContainer) {
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.setPauseUIUpdate(true);
+        }
+        return super.startActivitySafely(v, intent, item, sourceContainer);
+    }
+
+    @Override
+    protected void onActivityFlagsChanged(int changeBits) {
+        super.onActivityFlagsChanged(changeBits);
+        if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED
+                | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+            onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
+        }
+
+        if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
+                || (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
+            mHotseatPredictionController.setPauseUIUpdate(false);
+        }
+    }
+
+    @Override
+    public void folderCreatedFromItem(Folder folder, WorkspaceItemInfo itemInfo) {
+        super.folderCreatedFromItem(folder, itemInfo);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.folderCreatedFromWorkspaceItem(itemInfo, folder.getInfo());
+        }
+    }
+
+    @Override
+    public void folderConvertedToItem(Folder folder, WorkspaceItemInfo itemInfo) {
+        super.folderConvertedToItem(folder, itemInfo);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.folderConvertedToWorkspaceItem(itemInfo, folder.getInfo());
+        }
+    }
+
+    @Override
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        if (mHotseatPredictionController != null) {
+            return Stream.concat(super.getSupportedShortcuts(),
+                    Stream.of(mHotseatPredictionController));
+        } else {
+            return super.getSupportedShortcuts();
+        }
+    }
+
+    /**
+     * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
+     */
+    private void onStateOrResumeChanging(boolean inTransition) {
+        LauncherState state = getStateManager().getState();
+        DeviceProfile profile = getDeviceProfile();
+        boolean willUserBeActive = (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
+        boolean visible = (state == NORMAL || state == OVERVIEW)
+                && (willUserBeActive || isUserActive())
+                && !profile.isVerticalBarLayout()
+                && profile.isPhone && !profile.isLandscape;
+        UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
+                profile.hotseatBarSizePx);
+        if (state == NORMAL && !inTransition) {
+            ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
+        }
+    }
+
+    @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) {
+        super.bindPredictedItems(appInfos, ranks);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.showCachedItems(appInfos, ranks);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.destroy();
+            mHotseatPredictionController = null;
+        }
+    }
+
+    @Override
+    public void onStateSetEnd(LauncherState state) {
+        super.onStateSetEnd(state);
+
+        switch (state.ordinal) {
+            case HINT_STATE_ORDINAL: {
+                Workspace workspace = getWorkspace();
+                boolean willMoveScreens = workspace.getNextPage() != Workspace.DEFAULT_PAGE;
+                getStateManager().goToState(NORMAL, true,
+                        willMoveScreens ? null : getScrimView()::startDragHandleEducationAnim);
+                if (willMoveScreens) {
+                    workspace.post(workspace::moveToDefaultScreen);
+                }
+                break;
+            }
+            case OVERVIEW_STATE_ORDINAL: {
+                RecentsView recentsView = getOverviewPanel();
+                DiscoveryBounce.showForOverviewIfNeeded(this,
+                        recentsView.getPagedOrientationHandler());
+                RecentsView rv = getOverviewPanel();
+                sendCustomAccessibilityEvent(
+                        rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null);
+                break;
+            }
+            case QUICK_SWITCH_STATE_ORDINAL: {
+                RecentsView rv = getOverviewPanel();
+                TaskView tasktolaunch = rv.getTaskViewAt(0);
+                if (tasktolaunch != null) {
+                    tasktolaunch.launchTask(false, success -> {
+                        if (!success) {
+                            getStateManager().goToState(OVERVIEW);
+                            tasktolaunch.notifyTaskLaunchFailed(TAG);
+                        } else {
+                            getStateManager().moveToRestState();
+                        }
+                    }, MAIN_EXECUTOR.getHandler());
+                } else {
+                    getStateManager().goToState(NORMAL);
+                }
+                break;
+            }
+
+        }
+    }
+
+    @Override
+    public TouchController[] createTouchControllers() {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.1");
+        }
+        Mode mode = SysUINavigationMode.getMode(this);
+
+        ArrayList<TouchController> list = new ArrayList<>();
+        list.add(getDragController());
+        if (mode == NO_BUTTON) {
+            list.add(new NoButtonQuickSwitchTouchController(this));
+            list.add(new NavBarToHomeTouchController(this));
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.2");
+            }
+            if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+                if (TestProtocol.sDebugTracing) {
+                    Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.3");
+                }
+                list.add(new NoButtonNavbarToOverviewTouchController(this));
+            } else {
+                list.add(new FlingAndHoldTouchController(this));
+            }
+        } else {
+            if (getDeviceProfile().isVerticalBarLayout()) {
+                list.add(new OverviewToAllAppsTouchController(this));
+                list.add(new LandscapeEdgeSwipeController(this));
+                if (mode.hasGestures) {
+                    list.add(new TransposedQuickSwitchTouchController(this));
+                }
+            } else {
+                list.add(new PortraitStatesTouchController(this,
+                        mode.hasGestures /* allowDragToOverview */));
+                if (mode.hasGestures) {
+                    list.add(new QuickSwitchTouchController(this));
+                }
+            }
+        }
+
+        if (!getDeviceProfile().isMultiWindowMode) {
+            list.add(new StatusBarTouchController(this));
+        }
+
+        list.add(new LauncherTaskViewController(this));
+        return list.toArray(new TouchController[list.size()]);
+    }
+
+    @Override
+    public AtomicAnimationFactory createAtomicAnimationFactory() {
+        return new QuickstepAtomicAnimationFactory(this);
+    }
+
+    private static final class LauncherTaskViewController extends
+            TaskViewTouchController<Launcher> {
+
+        LauncherTaskViewController(Launcher activity) {
+            super(activity);
+        }
+
+        @Override
+        protected boolean isRecentsInteractive() {
+            return mActivity.isInState(OVERVIEW) || mActivity.isInState(OVERVIEW_MODAL_TASK);
+        }
+
+        @Override
+        protected boolean isRecentsModal() {
+            return mActivity.isInState(OVERVIEW_MODAL_TASK);
+        }
+
+        @Override
+        protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+            mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
+        }
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        super.dump(prefix, fd, writer, args);
+        RecentsView recentsView = getOverviewPanel();
+        writer.println("\nQuickstepLauncher:");
+        writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
+                recentsView.getPagedViewOrientedState()));
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
deleted file mode 100644
index cac170c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.Gravity;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
-import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.util.UiThreadHelper;
-import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.TouchInteractionService;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-import java.util.ArrayList;
-
-/**
- * Provides recents-related {@link UiFactory} logic and classes.
- */
-public abstract class RecentsUiFactory {
-
-    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
-    private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
-            WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
-
-    public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
-        @Override
-        public void mapRect(int left, int top, int right, int bottom, Rect out) {
-            out.left = top;
-            out.top = right;
-            out.right = bottom;
-            out.bottom = left;
-        }
-
-        @Override
-        public void mapInsets(Context context, Rect insets, Rect out) {
-            // If there is a display cutout, the top insets in portrait would also include the
-            // cutout, which we will get as the left inset in landscape. Using the max of left and
-            // top allows us to cover both cases (with or without cutout).
-            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
-                out.top = Math.max(insets.top, insets.left);
-                out.bottom = Math.max(insets.right, insets.bottom);
-                out.left = out.right = 0;
-            } else {
-                out.top = Math.max(insets.top, insets.left);
-                out.bottom = insets.right;
-                out.left = insets.bottom;
-                out.right = 0;
-            }
-        }
-    };
-
-    public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
-        @Override
-        public void mapRect(int left, int top, int right, int bottom, Rect out) {
-            out.left = bottom;
-            out.top = left;
-            out.right = top;
-            out.bottom = right;
-        }
-
-        @Override
-        public void mapInsets(Context context, Rect insets, Rect out) {
-            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
-                out.top = Math.max(insets.top, insets.right);
-                out.bottom = Math.max(insets.left, insets.bottom);
-                out.left = out.right = 0;
-            } else {
-                out.top = Math.max(insets.top, insets.right);
-                out.bottom = insets.left;
-                out.right = insets.bottom;
-                out.left = 0;
-            }
-        }
-
-        @Override
-        public int toNaturalGravity(int absoluteGravity) {
-            int horizontalGravity = absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
-            int verticalGravity = absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK;
-
-            if (horizontalGravity == Gravity.RIGHT) {
-                horizontalGravity = Gravity.LEFT;
-            } else if (horizontalGravity == Gravity.LEFT) {
-                horizontalGravity = Gravity.RIGHT;
-            }
-
-            if (verticalGravity == Gravity.TOP) {
-                verticalGravity = Gravity.BOTTOM;
-            } else if (verticalGravity == Gravity.BOTTOM) {
-                verticalGravity = Gravity.TOP;
-            }
-
-            return ((absoluteGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK)
-                    & ~Gravity.VERTICAL_GRAVITY_MASK)
-                    | horizontalGravity | verticalGravity;
-        }
-    };
-
-    public static RotationMode getRotationMode(DeviceProfile dp) {
-        return !dp.isVerticalBarLayout() ? RotationMode.NORMAL
-                : (dp.isSeascape() ? ROTATION_SEASCAPE : ROTATION_LANDSCAPE);
-    }
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        Mode mode = SysUINavigationMode.getMode(launcher);
-
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(launcher.getDragController());
-        if (mode == NO_BUTTON) {
-            list.add(new NoButtonQuickSwitchTouchController(launcher));
-            list.add(new NavBarToHomeTouchController(launcher));
-            list.add(new FlingAndHoldTouchController(launcher));
-        } else {
-            if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-                list.add(new OverviewToAllAppsTouchController(launcher));
-                list.add(new LandscapeEdgeSwipeController(launcher));
-                if (mode.hasGestures) {
-                    list.add(new TransposedQuickSwitchTouchController(launcher));
-                }
-            } else {
-                list.add(new PortraitStatesTouchController(launcher,
-                        mode.hasGestures /* allowDragToOverview */));
-                if (mode.hasGestures) {
-                    list.add(new QuickSwitchTouchController(launcher));
-                }
-            }
-        }
-
-        if (FeatureFlags.PULL_DOWN_STATUS_BAR
-                && !launcher.getDeviceProfile().isMultiWindowMode) {
-            list.add(new StatusBarTouchController(launcher));
-        }
-
-        list.add(new LauncherTaskViewController(launcher));
-        return list.toArray(new TouchController[list.size()]);
-    }
-
-    /**
-     * Creates and returns the controller responsible for recents view state transitions.
-     *
-     * @param launcher the launcher activity
-     * @return state handler for recents
-     */
-    public static StateHandler createRecentsViewStateController(Launcher launcher) {
-        return new RecentsViewStateController(launcher);
-    }
-
-    /**
-     * Clears the swipe shared state for the current swipe gesture.
-     */
-    public static void clearSwipeSharedState(boolean finishAnimation) {
-        TouchInteractionService.getSwipeSharedState().clearAllState(finishAnimation);
-    }
-
-    /**
-     * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
-     *
-     * @param launcher the launcher activity
-     */
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) {
-        LauncherState state = launcher.getStateManager().getState();
-        DeviceProfile profile = launcher.getDeviceProfile();
-        boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
-                && !profile.isVerticalBarLayout();
-        UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
-                visible ? 1 : 0, profile.hotseatBarSizePx);
-
-        if (state == NORMAL) {
-            launcher.<RecentsView>getOverviewPanel().setSwipeDownShouldLaunchApp(false);
-        }
-    }
-
-    private static final class LauncherTaskViewController extends
-            TaskViewTouchController<Launcher> {
-
-        LauncherTaskViewController(Launcher activity) {
-            super(activity);
-        }
-
-        @Override
-        protected boolean isRecentsInteractive() {
-            return mActivity.isInState(OVERVIEW);
-        }
-
-        @Override
-        protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
-            mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
-        }
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index b5d8424..085b9b3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,23 +15,25 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
+import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
 
-import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.os.Build;
 import android.util.FloatProperty;
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
@@ -44,7 +46,7 @@
 public final class RecentsViewStateController extends
         BaseRecentsViewStateController<LauncherRecentsView> {
 
-    public RecentsViewStateController(Launcher launcher) {
+    public RecentsViewStateController(BaseQuickstepLauncher launcher) {
         super(launcher);
     }
 
@@ -55,40 +57,40 @@
             mRecentsView.updateEmptyMessage();
             mRecentsView.resetTaskVisuals();
         }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state.getVisibleElements(mLauncher));
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
     }
 
     @Override
-    void setStateWithAnimationInternal(@NonNull final LauncherState toState,
-            @NonNull AnimatorSetBuilder builder, @NonNull AnimationConfig config) {
-        super.setStateWithAnimationInternal(toState, builder, config);
-
-        if (!toState.overviewUi) {
-            builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals);
-        }
+    void setStateWithAnimationInternal(@NonNull LauncherState toState,
+            @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
+        super.setStateWithAnimationInternal(toState, config, builder);
 
         if (toState.overviewUi) {
-            ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1);
-            updateAnim.addUpdateListener(valueAnimator -> {
-                // While animating into recents, update the visible task data as needed
-                mRecentsView.loadVisibleTaskData();
-            });
-            updateAnim.setDuration(config.duration);
-            builder.play(updateAnim);
+            // While animating into recents, update the visible task data as needed
+            builder.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
             mRecentsView.updateEmptyMessage();
+        } else {
+            builder.addListener(
+                    AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
         }
 
-        PropertySetter propertySetter = config.getPropertySetter(builder);
-        setAlphas(propertySetter, toState.getVisibleElements(mLauncher));
-        float fullscreenProgress = toState.getOverviewFullscreenProgress();
-        propertySetter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, fullscreenProgress, LINEAR);
+        setAlphas(builder, toState);
+        builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+                toState.getOverviewFullscreenProgress(), LINEAR);
     }
 
-    private void setAlphas(PropertySetter propertySetter, int visibleElements) {
-        boolean hasClearAllButton = (visibleElements & RECENTS_CLEAR_ALL_BUTTON) != 0;
+    private void setAlphas(PropertySetter propertySetter, LauncherState state) {
+        float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
-                hasClearAllButton ? 1f : 0f, LINEAR);
+                buttonAlpha, LINEAR);
+        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
+                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+    }
+
+    @Override
+    FloatProperty<RecentsView> getTaskModalnessProperty() {
+        return TASK_MODALNESS;
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 63ac528..8ff05f2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,36 +15,29 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+import android.content.Context;
 
-import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
 
 /**
  * State indicating that the Launcher is behind an app
  */
 public class BackgroundAppState extends OverviewState {
 
-    private static final int STATE_FLAGS =
-            FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY
-                    | FLAG_DISABLE_INTERACTION;
+    private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI
+            | FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS;
 
     public BackgroundAppState(int id) {
         this(id, LauncherLogProto.ContainerType.TASKSWITCHER);
     }
 
     protected BackgroundAppState(int id, int logContainer) {
-        super(id, logContainer, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
-    }
-
-    @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher, false);
+        super(id, logContainer, STATE_FLAGS);
     }
 
     @Override
@@ -52,8 +45,10 @@
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return super.getVerticalProgress(launcher);
         }
+        RecentsView recentsView = launcher.getOverviewPanel();
         int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher,
-                launcher.getDeviceProfile());
+                launcher.getDeviceProfile(),
+                recentsView.getPagedOrientationHandler());
         AllAppsTransitionController controller = launcher.getAllAppsController();
         float scrollRange = Math.max(controller.getShiftRange(), 1);
         float progressDelta = (transitionLength / scrollRange);
@@ -61,25 +56,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        // Initialize the recents view scale to what it would be when starting swipe up
-        RecentsView recentsView = launcher.getOverviewPanel();
-        int taskCount = recentsView.getTaskViewCount();
-        if (taskCount == 0) {
-            return super.getOverviewScaleAndTranslation(launcher);
-        }
-        TaskView dummyTask;
-        if (recentsView.getCurrentPage() >= 0) {
-            if (recentsView.getCurrentPage() <= taskCount - 1) {
-                dummyTask = recentsView.getCurrentPageTaskView();
-            } else {
-                dummyTask = recentsView.getTaskViewAt(taskCount - 1);
-            }
-        } else {
-            dummyTask = recentsView.getTaskViewAt(0);
-        }
-        return recentsView.getTempClipAnimationHelper().updateForFullscreenOverview(dummyTask)
-                .getScaleAndTranslation();
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return getOverviewScaleAndOffsetForBackgroundState(launcher);
     }
 
     @Override
@@ -90,18 +68,32 @@
     @Override
     public int getVisibleElements(Launcher launcher) {
         return super.getVisibleElements(launcher)
-                & ~RECENTS_CLEAR_ALL_BUTTON & ~VERTICAL_SWIPE_INDICATOR;
+                & ~OVERVIEW_BUTTONS & ~VERTICAL_SWIPE_INDICATOR;
     }
 
     @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
             // Translate hotseat offscreen if we show it in overview.
+            RecentsView recentsView = launcher.getOverviewPanel();
             ScaleAndTranslation scaleAndTranslation = super.getHotseatScaleAndTranslation(launcher);
             scaleAndTranslation.translationY += LayoutUtils.getShelfTrackingDistance(launcher,
-                    launcher.getDeviceProfile());
+                    launcher.getDeviceProfile(),
+                    recentsView.getPagedOrientationHandler());
             return scaleAndTranslation;
         }
         return super.getHotseatScaleAndTranslation(launcher);
     }
+
+    @Override
+    protected float getDepthUnchecked(Context context) {
+        return 1f;
+    }
+
+    public static float[] getOverviewScaleAndOffsetForBackgroundState(
+            BaseDraggingActivity activity) {
+        return new float[] {
+                ((RecentsView) activity.getOverviewPanel()).getMaxScaleForFullScreen(),
+                NO_OFFSET};
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
new file mode 100644
index 0000000..fc0dcd5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * An Overview state that shows the current task in a modal fashion. Modal state is where the
+ * current task is shown on its own without other tasks visible.
+ */
+public class OverviewModalTaskState extends OverviewState {
+
+    private static final int STATE_FLAGS =
+            FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE;
+
+    public OverviewModalTaskState(int id) {
+        super(id, ContainerType.OVERVIEW, STATE_FLAGS);
+    }
+
+    @Override
+    public int getTransitionDuration(Context launcher) {
+        return 300;
+    }
+
+    @Override
+    public int getVisibleElements(Launcher launcher) {
+        return OVERVIEW_BUTTONS;
+    }
+
+    @Override
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return getOverviewScaleAndOffsetForModalState(launcher);
+    }
+
+    @Override
+    public float getOverviewModalness() {
+        return 1.0f;
+    }
+
+    @Override
+    public void onBackPressed(Launcher launcher) {
+        launcher.getStateManager().goToState(LauncherState.OVERVIEW);
+        RecentsView recentsView = launcher.<RecentsView>getOverviewPanel();
+        if (recentsView != null) {
+            recentsView.resetModalVisuals();
+        } else {
+            super.onBackPressed(launcher);
+        }
+    }
+
+    public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) {
+        Rect out = new Rect();
+        activity.<RecentsView>getOverviewPanel().getTaskSize(out);
+        int taskHeight = out.height();
+        activity.<RecentsView>getOverviewPanel().getModalTaskSize(out);
+        int newHeight = out.height();
+
+        float scale = (float) newHeight / taskHeight;
+
+        return new float[] {scale, NO_OFFSET};
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
index 427206a..fc9a11b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
@@ -14,38 +14,18 @@
  * limitations under the License.
  */
 package com.android.launcher3.uioverrides.states;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCRIM_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 
 public class OverviewPeekState extends OverviewState {
+    private static final float OVERVIEW_OFFSET = 0.7f;
+
     public OverviewPeekState(int id) {
         super(id);
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        ScaleAndTranslation result = super.getOverviewScaleAndTranslation(launcher);
-        result.translationX = NORMAL.getOverviewScaleAndTranslation(launcher).translationX
-                - launcher.getResources().getDimension(R.dimen.overview_peek_distance);
-        return result;
-    }
-
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            AnimatorSetBuilder builder) {
-        if (this == OVERVIEW_PEEK && fromState == NORMAL) {
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_SCRIM_FADE, FAST_OUT_SLOW_IN);
-        }
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, OVERVIEW_OFFSET};
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index ed5dba1..d174bfd 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -15,35 +15,22 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static android.view.View.VISIBLE;
-
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
+import android.content.Context;
 import android.graphics.Rect;
 import android.view.View;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -56,24 +43,29 @@
  */
 public class OverviewState extends LauncherState {
 
-    // Scale recents takes before animating in
-    private static final float RECENTS_PREPARE_SCALE = 1.33f;
-
     protected static final Rect sTempRect = new Rect();
 
     private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
-            | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY;
+            | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE
+            | FLAG_CLOSE_POPUPS;
 
     public OverviewState(int id) {
-        this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
+        this(id, STATE_FLAGS);
     }
 
-    protected OverviewState(int id, int transitionDuration, int stateFlags) {
-        this(id, ContainerType.TASKSWITCHER, transitionDuration, stateFlags);
+    protected OverviewState(int id, int stateFlags) {
+        this(id, ContainerType.TASKSWITCHER, stateFlags);
     }
 
-    protected OverviewState(int id, int logContainer, int transitionDuration, int stateFlags) {
-        super(id, logContainer, transitionDuration, stateFlags);
+    protected OverviewState(int id, int logContainer, int stateFlags) {
+        super(id, logContainer, stateFlags);
+    }
+
+    @Override
+    public int getTransitionDuration(Context context) {
+        // In no-button mode, overview comes in all the way from the left, so give it more time.
+        boolean isNoButtonMode = SysUINavigationMode.INSTANCE.get(context).getMode() == NO_BUTTON;
+        return isNoButtonMode && ENABLE_OVERVIEW_ACTIONS.get() ? 380 : 250;
     }
 
     @Override
@@ -110,19 +102,18 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1f, 0f, 0f);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, NO_OFFSET};
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher);
-    }
-
-    @Override
-    public void onStateTransitionEnd(Launcher launcher) {
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
-        DiscoveryBounce.showForOverviewIfNeeded(launcher);
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        if (this == OVERVIEW && ENABLE_OVERVIEW_ACTIONS.get()
+                && removeShelfFromOverview(launcher)) {
+            // Treat the QSB as part of the hotseat so they move together.
+            return getHotseatScaleAndTranslation(launcher);
+        }
+        return super.getQsbScaleAndTranslation(launcher);
     }
 
     @Override
@@ -137,13 +128,17 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
+        RecentsView recentsView = launcher.getOverviewPanel();
+        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(launcher) ||
+                hideShelfInTwoButtonLandscape(launcher, recentsView.getPagedOrientationHandler())) {
+            return OVERVIEW_BUTTONS;
+        } else if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+            return VERTICAL_SWIPE_INDICATOR | OVERVIEW_BUTTONS;
         } else {
             boolean hasAllAppsHeaderExtra = launcher.getAppsView() != null
                     && launcher.getAppsView().getFloatingHeaderView().hasVisibleContent();
-            return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
-                    (hasAllAppsHeaderExtra ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
+            return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | OVERVIEW_BUTTONS
+                    | (hasAllAppsHeaderExtra ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
         }
     }
 
@@ -168,7 +163,7 @@
 
     @Override
     public String getDescription(Launcher launcher) {
-        return launcher.getString(R.string.accessibility_desc_recent_apps);
+        return launcher.getString(R.string.accessibility_recent_apps);
     }
 
     public static float getDefaultSwipeHeight(Launcher launcher) {
@@ -176,6 +171,11 @@
     }
 
     @Override
+    protected float getDepthUnchecked(Context context) {
+        return 1f;
+    }
+
+    @Override
     public void onBackPressed(Launcher launcher) {
         TaskView taskView = launcher.<RecentsView>getOverviewPanel().getRunningTaskView();
         if (taskView != null) {
@@ -187,30 +187,6 @@
         }
     }
 
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            AnimatorSetBuilder builder) {
-        if (fromState == NORMAL && this == OVERVIEW) {
-            if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL);
-                builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
-            } else {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
-
-                // Scale up the recents, if it is not coming from the side
-                RecentsView overview = launcher.getOverviewPanel();
-                if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
-                    SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
-                }
-            }
-            builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
-        }
-    }
-
     public static OverviewState newBackgroundState(int id) {
         return new BackgroundAppState(id);
     }
@@ -222,4 +198,11 @@
     public static OverviewState newSwitchState(int id) {
         return new QuickSwitchState(id);
     }
+
+    /**
+     *  New Overview substate that represents the overview in modal mode (one task shown on its own)
+     */
+    public static OverviewState newModalTaskState(int id) {
+        return new OverviewModalTaskState(id);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index 6c9f46f..2c7373e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -15,23 +15,16 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import android.os.Handler;
-import android.os.Looper;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
 
 /**
  * State to indicate we are about to launch a recent task. Note that this state is only used when
- * quick switching from launcher; quick switching from an app uses WindowTransformSwipeHelper.
- * @see com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget#NEW_TASK
+ * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler.
+ * @see com.android.quickstep.GestureState.GestureEndTarget#NEW_TASK
  */
 public class QuickSwitchState extends BackgroundAppState {
 
-    private static final String TAG = "QuickSwitchState";
-
     public QuickSwitchState(int id) {
         super(id, LauncherLogProto.ContainerType.APP);
     }
@@ -48,21 +41,4 @@
     public int getVisibleElements(Launcher launcher) {
         return NONE;
     }
-
-    @Override
-    public void onStateTransitionEnd(Launcher launcher) {
-        TaskView tasktolaunch = launcher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
-        if (tasktolaunch != null) {
-            tasktolaunch.launchTask(false, success -> {
-                if (!success) {
-                    launcher.getStateManager().goToState(OVERVIEW);
-                    tasktolaunch.notifyTaskLaunchFailed(TAG);
-                } else {
-                    launcher.getStateManager().moveToRestState();
-                }
-            }, new Handler(Looper.getMainLooper()));
-        } else {
-            launcher.getStateManager().goToState(NORMAL);
-        }
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
new file mode 100644
index 0000000..a0af797
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static android.view.View.VISIBLE;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+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.LauncherState.OVERVIEW_PEEK;
+import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+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_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
+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_SCRIM_FADE;
+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_VERTICAL_PROGRESS;
+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;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Animation factory for quickstep specific transitions
+ */
+public class QuickstepAtomicAnimationFactory extends
+        RecentsAtomicAnimationFactory<Launcher, LauncherState> {
+
+    // Scale recents takes before animating in
+    private static final float RECENTS_PREPARE_SCALE = 1.33f;
+
+    public static final int INDEX_SHELF_ANIM = RecentsAtomicAnimationFactory.NEXT_INDEX + 0;
+    public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM =
+            RecentsAtomicAnimationFactory.NEXT_INDEX + 1;
+
+    private static final int MY_ANIM_COUNT = 2;
+    protected static final int NEXT_INDEX = RecentsAtomicAnimationFactory.NEXT_INDEX
+            + MY_ANIM_COUNT;
+
+    // Due to use of physics, duration may differ between devices so we need to calculate and
+    // cache the value.
+    private int mHintToNormalDuration = -1;
+
+    public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
+
+    public QuickstepAtomicAnimationFactory(QuickstepLauncher activity) {
+        super(activity, MY_ANIM_COUNT);
+    }
+
+    @Override
+    public Animator createStateElementAnimation(int index, float... values) {
+        switch (index) {
+            case INDEX_SHELF_ANIM: {
+                AllAppsTransitionController aatc = mActivity.getAllAppsController();
+                Animator springAnim = aatc.createSpringAnimation(values);
+
+                if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
+                    // Translate hotseat with the shelf until reaching overview.
+                    float overviewProgress = OVERVIEW.getVerticalProgress(mActivity);
+                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mActivity);
+                    float shiftRange = aatc.getShiftRange();
+                    if (values.length == 1) {
+                        values = new float[] {aatc.getProgress(), values[0]};
+                    }
+                    ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
+                    hotseatAnim.addUpdateListener(anim -> {
+                        float progress = (Float) anim.getAnimatedValue();
+                        if (progress >= overviewProgress || mActivity.isInState(BACKGROUND_APP)) {
+                            float hotseatShift = (progress - overviewProgress) * shiftRange;
+                            mActivity.getHotseat().setTranslationY(hotseatShift + sat.translationY);
+                        }
+                    });
+                    hotseatAnim.setInterpolator(LINEAR);
+                    hotseatAnim.setDuration(springAnim.getDuration());
+
+                    AnimatorSet anim = new AnimatorSet();
+                    anim.play(hotseatAnim);
+                    anim.play(springAnim);
+                    return anim;
+                }
+
+                return springAnim;
+            }
+            case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
+                StateAnimationConfig config = new StateAnimationConfig();
+                config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
+
+                config.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
+                config.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_3);
+                if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
+                    config.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
+                    config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
+                }
+
+                StateManager<LauncherState> stateManager = mActivity.getStateManager();
+                return stateManager.createAtomicAnimation(
+                        stateManager.getCurrentStableState(), OVERVIEW, config);
+            }
+            default:
+                return super.createStateElementAnimation(index, values);
+        }
+    }
+
+    @Override
+    public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
+            StateAnimationConfig config) {
+        if (toState == NORMAL && fromState == OVERVIEW) {
+            config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
+            config.setInterpolator(ANIM_WORKSPACE_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);
+            config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
+            Workspace workspace = mActivity.getWorkspace();
+
+            // Start from a higher workspace scale, but only if we're invisible so we don't jump.
+            boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
+            if (isWorkspaceVisible) {
+                CellLayout currentChild = (CellLayout) workspace.getChildAt(
+                        workspace.getCurrentPage());
+                isWorkspaceVisible = currentChild.getVisibility() == VISIBLE
+                        && currentChild.getShortcutsAndWidgets().getAlpha() > 0;
+            }
+            if (!isWorkspaceVisible) {
+                workspace.setScaleX(0.92f);
+                workspace.setScaleY(0.92f);
+            }
+            Hotseat hotseat = mActivity.getHotseat();
+            boolean isHotseatVisible = hotseat.getVisibility() == VISIBLE && hotseat.getAlpha() > 0;
+            if (!isHotseatVisible) {
+                hotseat.setScaleX(0.92f);
+                hotseat.setScaleY(0.92f);
+                if (ENABLE_OVERVIEW_ACTIONS.get()) {
+                    AllAppsContainerView qsbContainer = mActivity.getAppsView();
+                    View qsb = qsbContainer.getSearchView();
+                    boolean qsbVisible = qsb.getVisibility() == VISIBLE && qsb.getAlpha() > 0;
+                    if (!qsbVisible) {
+                        qsbContainer.setScaleX(0.92f);
+                        qsbContainer.setScaleY(0.92f);
+                    }
+                }
+            }
+        } else if (toState == NORMAL && fromState == OVERVIEW_PEEK) {
+            // Keep fully visible until the very end (when overview is offscreen) to make invisible.
+            config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
+        } else if (toState == OVERVIEW_PEEK && fromState == NORMAL) {
+            config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
+            config.setInterpolator(ANIM_OVERVIEW_SCRIM_FADE, FAST_OUT_SLOW_IN);
+        } else if ((fromState == NORMAL || fromState == HINT_STATE) && toState == OVERVIEW) {
+            if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
+                config.setInterpolator(ANIM_WORKSPACE_SCALE,
+                        fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
+                config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+            } else {
+                config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
+
+                // Scale up the recents, if it is not coming from the side
+                RecentsView overview = mActivity.getOverviewPanel();
+                if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
+                    SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+                }
+            }
+            config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
+            config.setInterpolator(ANIM_ALL_APPS_FADE, OVERSHOOT_1_2);
+            config.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
+            config.setInterpolator(ANIM_DEPTH, OVERSHOOT_1_2);
+            Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get()
+                    && removeShelfFromOverview(mActivity)
+                    ? OVERSHOOT_1_2
+                    : OVERSHOOT_1_7;
+            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
+            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
+            config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
+        } else if (fromState == HINT_STATE && toState == NORMAL) {
+            config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
+            if (mHintToNormalDuration == -1) {
+                ValueAnimator va = getSpringScaleAnimator(mActivity, mActivity.getWorkspace(),
+                        toState.getWorkspaceScaleAndTranslation(mActivity).scale);
+                mHintToNormalDuration = (int) va.getDuration();
+            }
+            config.duration = Math.max(config.duration, mHintToNormalDuration);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index 3231f37..fac478e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -16,38 +16,43 @@
 
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_PAUSE_TO_OVERVIEW_ANIM;
 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_PEEK;
-import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_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;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_PAUSE_TO_OVERVIEW_ANIM;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppTransitionManagerImpl;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.views.RecentsView;
 
@@ -60,7 +65,7 @@
     private static final long PEEK_OUT_ANIM_DURATION = 100;
     private static final float MAX_DISPLACEMENT_PERCENT = 0.75f;
 
-    private final MotionPauseDetector mMotionPauseDetector;
+    protected final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
     private final float mMotionPauseMaxDisplacement;
 
@@ -70,61 +75,75 @@
         super(l, false /* allowDragToOverview */);
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
-        mMotionPauseMaxDisplacement = getShiftRange() * MAX_DISPLACEMENT_PERCENT;
+        mMotionPauseMaxDisplacement = getMotionPauseMaxDisplacement();
+    }
+
+    protected float getMotionPauseMaxDisplacement() {
+        return getShiftRange() * MAX_DISPLACEMENT_PERCENT;
     }
 
     @Override
     protected long getAtomicDuration() {
-        return LauncherAppTransitionManagerImpl.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
+        return QuickstepAtomicAnimationFactory.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         mMotionPauseDetector.clear();
 
-        super.onDragStart(start);
+        super.onDragStart(start, startDisplacement);
 
         if (handlingOverviewAnim()) {
-            mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
-                RecentsView recentsView = mLauncher.getOverviewPanel();
-                recentsView.setOverviewStateEnabled(isPaused);
-                if (mPeekAnim != null) {
-                    mPeekAnim.cancel();
-                }
-                LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
-                LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
-                long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
-                mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState,
-                        new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration);
-                mPeekAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mPeekAnim = null;
-                    }
-                });
-                mPeekAnim.start();
-                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
-
-                mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
-                        peekDuration, 0);
-            });
+            mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
         }
+
+        if (mAtomicAnim != null) {
+            mAtomicAnim.cancel();
+        }
+    }
+
+    protected void onMotionPauseChanged(boolean isPaused) {
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        recentsView.setOverviewStateEnabled(isPaused);
+        if (mPeekAnim != null) {
+            mPeekAnim.cancel();
+        }
+        LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
+        LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
+        long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
+
+        StateAnimationConfig config = new StateAnimationConfig();
+        config.duration = peekDuration;
+        config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
+        mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(
+                fromState, toState, config);
+        mPeekAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mPeekAnim = null;
+            }
+        });
+        mPeekAnim.start();
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+
+        mLauncher.getDragLayer().getScrim().createSysuiMultiplierAnim(isPaused ? 0 : 1)
+                .setDuration(peekDuration).start();
     }
 
     /**
      * @return Whether we are handling the overview animation, rather than
      * having it as part of the existing animation to the target state.
      */
-    private boolean handlingOverviewAnim() {
-        int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+    protected boolean handlingOverviewAnim() {
+        int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         return mStartState == NORMAL && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
     }
 
     @Override
-    protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState,
-            LauncherState toState) {
+    protected StateAnimationConfig getConfigForStates(
+            LauncherState fromState, LauncherState toState) {
         if (fromState == NORMAL && toState == ALL_APPS) {
-            AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            StateAnimationConfig builder = new StateAnimationConfig();
             // Fade in prediction icons quickly, then rest of all apps after reaching overview.
             float progressToReachOverview = NORMAL.getVerticalProgress(mLauncher)
                     - OVERVIEW.getVerticalProgress(mLauncher);
@@ -143,7 +162,7 @@
             builder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_3);
             return builder;
         } else if (fromState == ALL_APPS && toState == NORMAL) {
-            AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            StateAnimationConfig builder = new StateAnimationConfig();
             // Keep all apps/predictions opaque until the very end of the transition.
             float progressToReachOverview = OVERVIEW.getVerticalProgress(mLauncher);
             builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
@@ -156,34 +175,26 @@
                     1));
             return builder;
         }
-        return super.getAnimatorSetBuilderForStates(fromState, toState);
+        return super.getConfigForStates(fromState, toState);
     }
 
     @Override
     public boolean onDrag(float displacement, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "FlingAndHoldTouchController");
+        }
         float upDisplacement = -displacement;
-        mMotionPauseDetector.setDisallowPause(upDisplacement < mMotionPauseMinDisplacement
+        mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
+                || upDisplacement < mMotionPauseMinDisplacement
                 || upDisplacement > mMotionPauseMaxDisplacement);
-        mMotionPauseDetector.addPosition(displacement, event.getEventTime());
+        mMotionPauseDetector.addPosition(event);
         return super.onDrag(displacement, event);
     }
 
     @Override
     public void onDragEnd(float velocity) {
         if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
-            if (mPeekAnim != null) {
-                mPeekAnim.cancel();
-            }
-
-            Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
-                    INDEX_PAUSE_TO_OVERVIEW_ANIM);
-            overviewAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
-                }
-            });
-            overviewAnim.start();
+            goToOverviewOnDragEnd(velocity);
         } else {
             super.onDragEnd(velocity);
         }
@@ -195,6 +206,38 @@
         mMotionPauseDetector.clear();
     }
 
+    protected void goToOverviewOnDragEnd(float velocity) {
+        if (mPeekAnim != null) {
+            mPeekAnim.cancel();
+        }
+
+        Animator overviewAnim = mLauncher.createAtomicAnimationFactory()
+                .createStateElementAnimation(INDEX_PAUSE_TO_OVERVIEW_ANIM);
+        mAtomicAnim = new AnimatorSet();
+        mAtomicAnim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                if (mCancelled) {
+                    StateAnimationConfig config = new StateAnimationConfig();
+                    config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
+                    config.duration = PEEK_OUT_ANIM_DURATION;
+                    mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(
+                            mFromState, mToState, config);
+                    mPeekAnim.start();
+                }
+                mAtomicAnim = null;
+            }
+        });
+        mAtomicAnim.play(overviewAnim);
+        mAtomicAnim.start();
+    }
+
     @Override
     protected void goToTargetState(LauncherState targetState, int logAction) {
         if (mPeekAnim != null && mPeekAnim.isStarted()) {
@@ -211,11 +254,14 @@
     }
 
     @Override
-    protected void updateAnimatorBuilderOnReinit(AnimatorSetBuilder builder) {
+    @AnimationFlags
+    protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) {
         if (handlingOverviewAnim()) {
             // We don't want the state transition to all apps to animate overview,
             // as that will cause a jump after our atomic animation.
-            builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
+            return animComponents | SKIP_OVERVIEW;
+        } else {
+            return animComponents;
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index ef50c7b..c1a585e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -15,36 +15,39 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static android.view.View.TRANSLATION_X;
-
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.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.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.BaseFlags;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -98,18 +101,37 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NavBarToHomeTouchController.canInterceptTouch "
+                    + ev);
+        }
         boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
         if (!cameFromNavBar) {
             return false;
         }
-        if (mStartState == OVERVIEW || mStartState == ALL_APPS) {
+        if (mStartState.overviewUi || mStartState == ALL_APPS) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
+                        "NavBarToHomeTouchController.canInterceptTouch true 1 "
+                                + mStartState.overviewUi + " " + (mStartState == ALL_APPS));
+            }
             return true;
         }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+        int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL;
+        if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
+                        "NavBarToHomeTouchController.canInterceptTouch true 2 "
+                                + AbstractFloatingView.getTopOpenView(mLauncher), new Exception());
+            }
             return true;
         }
-        if (BaseFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+        if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
                 && AssistantUtilities.isExcludedAssistantRunning()) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
+                        "NavBarToHomeTouchController.canInterceptTouch true 3");
+            }
             return true;
         }
         return false;
@@ -125,47 +147,40 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         initCurrentAnimation();
     }
 
     private void initCurrentAnimation() {
         long accuracy = (long) (getShiftRange() * 2);
-        final AnimatorSet anim = new AnimatorSet();
-        if (mStartState == OVERVIEW) {
+        final PendingAnimation builder = new PendingAnimation(accuracy);
+        if (mStartState.overviewUi) {
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            float pullbackDist = mPullbackDistance;
-            if (!recentsView.isRtl()) {
-                pullbackDist = -pullbackDist;
+            builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
+                    -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                builder.addOnFrameCallback(
+                        () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
             }
-            Animator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X, pullbackDist);
-            pullback.setInterpolator(PULLBACK_INTERPOLATOR);
-            anim.play(pullback);
         } else if (mStartState == ALL_APPS) {
-            AnimatorSetBuilder builder = new AnimatorSetBuilder();
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
-            Animator allAppsProgress = ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS,
-                    -mPullbackDistance / allAppsController.getShiftRange());
-            allAppsProgress.setInterpolator(PULLBACK_INTERPOLATOR);
-            builder.play(allAppsProgress);
+            builder.setFloat(allAppsController, ALL_APPS_PROGRESS,
+                    -mPullbackDistance / allAppsController.getShiftRange(), PULLBACK_INTERPOLATOR);
+
             // Slightly fade out all apps content to further distinguish from scrolling.
-            builder.setInterpolator(AnimatorSetBuilder.ANIM_ALL_APPS_FADE, Interpolators
-                    .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
-            AnimationConfig config = new AnimationConfig();
+            StateAnimationConfig config = new StateAnimationConfig();
             config.duration = accuracy;
-            allAppsController.setAlphas(mEndState.getVisibleElements(mLauncher), config, builder);
-            anim.play(builder.build());
+            config.setInterpolator(StateAnimationConfig.ANIM_ALL_APPS_FADE, Interpolators
+                    .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
+
+            allAppsController.setAlphas(mEndState, config, builder);
         }
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher);
         if (topView != null) {
-            Animator hintCloseAnim = topView.createHintCloseAnim(mPullbackDistance);
-            if (hintCloseAnim != null) {
-                hintCloseAnim.setInterpolator(PULLBACK_INTERPOLATOR);
-                anim.play(hintCloseAnim);
-            }
+            topView.addHintCloseAnim(mPullbackDistance, PULLBACK_INTERPOLATOR, builder);
         }
-        anim.setDuration(accuracy);
-        mCurrentAnimation = AnimatorPlaybackController.wrap(anim, accuracy, this::clearState);
+        mCurrentAnimation = builder.createPlaybackController()
+                .setOnCancelRunnable(this::clearState);
     }
 
     private void clearState() {
@@ -192,6 +207,11 @@
         boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
                 || (velocity < 0 && fling);
         if (success) {
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                RecentsView recentsView = mLauncher.getOverviewPanel();
+                recentsView.switchToScreenshot(null,
+                        () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
+            }
             mLauncher.getStateManager().goToState(mEndState, true,
                     () -> onSwipeInteractionCompleted(mEndState));
             if (mStartState != mEndState) {
@@ -203,17 +223,13 @@
                 logStateChange(topOpenView.getLogContainerType(), logAction);
             }
             ActivityManagerWrapper.getInstance()
-                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         } else {
             // Quickly return to the state we came from (we didn't move far).
             ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
             anim.setFloatValues(progress, 0);
-            anim.addListener(new AnimationSuccessListener() {
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    onSwipeInteractionCompleted(mStartState);
-                }
-            });
+            anim.addListener(AnimationSuccessListener.forRunnable(
+                    () -> onSwipeInteractionCompleted(mStartState)));
             anim.setDuration(80).start();
         }
     }
@@ -232,5 +248,9 @@
                 startContainerType,
                 mEndState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
+                .withDstState(StatsLogManager.containerTypeToAtomState(mEndState.containerType))
+                .log(LAUNCHER_HOME_GESTURE);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
new file mode 100644
index 0000000..9316938
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
+ * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
+ * first home screen instead of to Overview.
+ */
+public class NoButtonNavbarToOverviewTouchController extends FlingAndHoldTouchController {
+
+
+    // How much of the movement to use for translating overview after swipe and hold.
+    private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
+    private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
+    private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
+
+    private final RecentsView mRecentsView;
+
+    private boolean mDidTouchStartInNavBar;
+    private boolean mReachedOverview;
+    private boolean mIsOverviewRehidden;
+    private boolean mIsHomeStaggeredAnimFinished;
+    // The last recorded displacement before we reached overview.
+    private PointF mStartDisplacement = new PointF();
+
+    // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
+    private ObjectAnimator mNormalToHintOverviewScrimAnimator;
+
+    public NoButtonNavbarToOverviewTouchController(Launcher l) {
+        super(l);
+        mRecentsView = l.getOverviewPanel();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController.ctor");
+        }
+    }
+
+    @Override
+    protected float getMotionPauseMaxDisplacement() {
+        // No need to disallow pause when swiping up all the way up the screen (unlike
+        // FlingAndHoldTouchController where user is probably intending to go to all apps).
+        return Float.MAX_VALUE;
+    }
+
+    @Override
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+        return super.canInterceptTouch(ev);
+    }
+
+    @Override
+    protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+        if (fromState == NORMAL && mDidTouchStartInNavBar) {
+            return HINT_STATE;
+        } else if (fromState == OVERVIEW && isDragTowardPositive) {
+            // Don't allow swiping up to all apps.
+            return OVERVIEW;
+        }
+        return super.getTargetState(fromState, isDragTowardPositive);
+    }
+
+    @Override
+    protected float initCurrentAnimation(int animComponents) {
+        float progressMultiplier = super.initCurrentAnimation(animComponents);
+        if (mToState == HINT_STATE) {
+            // Track the drag across the entire height of the screen.
+            progressMultiplier = -1 / getShiftRange();
+        }
+        return progressMultiplier;
+    }
+
+    @Override
+    public void onDragStart(boolean start, float startDisplacement) {
+        super.onDragStart(start, startDisplacement);
+        if (mFromState == NORMAL && mToState == HINT_STATE) {
+            mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofFloat(
+                    mLauncher.getDragLayer().getOverviewScrim(),
+                    OverviewScrim.SCRIM_PROGRESS,
+                    mFromState.getOverviewScrimAlpha(mLauncher),
+                    mToState.getOverviewScrimAlpha(mLauncher));
+        }
+        mReachedOverview = false;
+    }
+
+    @Override
+    protected void updateProgress(float fraction) {
+        super.updateProgress(fraction);
+        if (mNormalToHintOverviewScrimAnimator != null) {
+            mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
+        }
+    }
+
+    @Override
+    public void onDragEnd(float velocity) {
+        super.onDragEnd(velocity);
+        mNormalToHintOverviewScrimAnimator = null;
+    }
+
+    @Override
+    protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
+            LauncherState targetState, float velocity, boolean isFling) {
+        super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
+                isFling);
+        if (targetState == HINT_STATE) {
+            // Normally we compute the duration based on the velocity and distance to the given
+            // state, but since the hint state tracks the entire screen without a clear endpoint, we
+            // need to manually set the duration to a reasonable value.
+            animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher));
+        }
+    }
+
+    @Override
+    protected void onMotionPauseChanged(boolean isPaused) {
+        if (mCurrentAnimation == null) {
+            return;
+        }
+        mNormalToHintOverviewScrimAnimator = null;
+        mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
+            mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
+                mReachedOverview = true;
+                maybeSwipeInteractionToOverviewComplete();
+            });
+        });
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+    }
+
+    private void maybeSwipeInteractionToOverviewComplete() {
+        if (mReachedOverview && mDetector.isSettlingState()) {
+            onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
+        }
+    }
+
+    // Used if flinging back to home after reaching overview
+    private void maybeSwipeInteractionToHomeComplete() {
+        if (mIsHomeStaggeredAnimFinished && mIsOverviewRehidden) {
+            onSwipeInteractionCompleted(NORMAL, Touch.FLING);
+        }
+    }
+
+    @Override
+    protected boolean handlingOverviewAnim() {
+        return mDidTouchStartInNavBar && super.handlingOverviewAnim();
+    }
+
+    @Override
+    public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController");
+        }
+        if (mMotionPauseDetector.isPaused()) {
+            if (!mReachedOverview) {
+                mStartDisplacement.set(xDisplacement, yDisplacement);
+            } else {
+                mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
+                        * OVERVIEW_MOVEMENT_FACTOR);
+                mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
+                        * OVERVIEW_MOVEMENT_FACTOR);
+            }
+            // Stay in Overview.
+            return true;
+        }
+        return super.onDrag(yDisplacement, xDisplacement, event);
+    }
+
+    @Override
+    protected void goToOverviewOnDragEnd(float velocity) {
+        float velocityDp = dpiFromPx(velocity);
+        boolean isFling = Math.abs(velocityDp) > 1;
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        boolean goToHomeInsteadOfOverview = isFling;
+        if (goToHomeInsteadOfOverview) {
+            if (velocity > 0) {
+                stateManager.goToState(NORMAL, true,
+                        () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
+            } else {
+                mIsHomeStaggeredAnimFinished = mIsOverviewRehidden = false;
+
+                StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
+                        mLauncher, velocity, false /* animateOverviewScrim */);
+                staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
+                    @Override
+                    public void onAnimationSuccess(Animator animator) {
+                        mIsHomeStaggeredAnimFinished = true;
+                        maybeSwipeInteractionToHomeComplete();
+                    }
+                }).start();
+
+                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
+                stateManager.cancelAnimation();
+                StateAnimationConfig config = new StateAnimationConfig();
+                config.duration = OVERVIEW.getTransitionDuration(mLauncher);
+                config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
+                AnimatorSet anim = stateManager.createAtomicAnimation(
+                        stateManager.getState(), NORMAL, config);
+                anim.addListener(AnimationSuccessListener.forRunnable(() -> {
+                    mIsOverviewRehidden = true;
+                    maybeSwipeInteractionToHomeComplete();
+                }));
+                anim.start();
+            }
+        }
+        if (mReachedOverview) {
+            float distanceDp = dpiFromPx(Math.max(
+                    Math.abs(mRecentsView.getTranslationX()),
+                    Math.abs(mRecentsView.getTranslationY())));
+            long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
+                    distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
+            mRecentsView.animate()
+                    .translationX(0)
+                    .translationY(0)
+                    .setInterpolator(ACCEL_DEACCEL)
+                    .setDuration(duration)
+                    .withEndAction(goToHomeInsteadOfOverview
+                            ? null
+                            : this::maybeSwipeInteractionToOverviewComplete);
+        }
+    }
+
+    private float dpiFromPx(float pixels) {
+        return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics());
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 76374af..1b439d1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -16,54 +16,57 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_PAUSE_TO_OVERVIEW_ANIM;
 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.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
-import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_5;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
+import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
 import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
+import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_PAUSE_TO_OVERVIEW_ANIM;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.BothAxesSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -71,7 +74,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.ShelfPeekAnim;
@@ -92,17 +95,18 @@
     private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
     private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL;
 
-    private final Launcher mLauncher;
+    private final BaseQuickstepLauncher mLauncher;
     private final BothAxesSwipeDetector mSwipeDetector;
+    private final ShelfPeekAnim mShelfPeekAnim;
     private final float mXRange;
     private final float mYRange;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
+    private final LauncherRecentsView mRecentsView;
 
     private boolean mNoIntercept;
     private LauncherState mStartState;
 
-    private ShelfPeekAnim mShelfPeekAnim;
     private boolean mIsHomeScreenVisible = true;
 
     // As we drag, we control 3 animations: one to get non-overview components out of the way,
@@ -111,11 +115,14 @@
     private AnimatorPlaybackController mXOverviewAnim;
     private AnimatorPlaybackController mYOverviewAnim;
 
-    public NoButtonQuickSwitchTouchController(Launcher launcher) {
+    public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
         mLauncher = launcher;
         mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
+        mShelfPeekAnim = mLauncher.getShelfPeekAnim();
+        mRecentsView = mLauncher.getOverviewPanel();
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
-        mYRange = LayoutUtils.getShelfTrackingDistance(mLauncher, mLauncher.getDeviceProfile());
+        mYRange = LayoutUtils.getShelfTrackingDistance(
+            mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
@@ -154,7 +161,7 @@
         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
             return false;
         }
-        int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+        int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
             return false;
         }
@@ -165,9 +172,6 @@
     public void onDragStart(boolean start) {
         mMotionPauseDetector.clear();
         if (start) {
-            mShelfPeekAnim = ((QuickstepAppTransitionManagerImpl) mLauncher
-                    .getAppTransitionManager()).getShelfPeekAnim();
-
             mStartState = mLauncher.getStateManager().getState();
 
             mMotionPauseDetector.setOnMotionPauseListener(this);
@@ -182,14 +186,18 @@
 
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+
+        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            return;
+        }
+
         ShelfAnimState shelfState = isPaused ? PEEK : HIDE;
         if (shelfState == PEEK) {
             // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking.
-            AnimatorSetBuilder builder = new AnimatorSetBuilder();
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
-            allAppsController.setAlphas(NORMAL.getVisibleElements(mLauncher),
-                    new AnimationConfig(), builder);
-            builder.build().setDuration(0).start();
+            allAppsController.setAlphas(
+                    NORMAL, new StateAnimationConfig(), NO_ANIM_PROPERTY_SETTER);
 
             if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
                 // Hotseat was hidden, but we need it visible when peeking.
@@ -198,86 +206,70 @@
         }
         mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR,
                 ShelfPeekAnim.DURATION);
-        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
     }
 
     private void setupAnimators() {
         // Animate the non-overview components (e.g. workspace, shelf) out of the way.
-        AnimatorSetBuilder nonOverviewBuilder = new AnimatorSetBuilder();
+        StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
         nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
         nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, TRANSLATE_OUT_INTERPOLATOR);
         nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
-        updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder, ANIM_ALL);
+        updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder);
         mNonOverviewAnim.dispatchOnStart();
 
+        if (mRecentsView.getTaskViewCount() == 0) {
+            mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
+                if (!isEmpty && mSwipeDetector.isDraggingState()) {
+                    // We have loaded tasks, update the animators to start at the correct scale etc.
+                    setupOverviewAnimators();
+                }
+            });
+        }
+
         setupOverviewAnimators();
     }
 
     /** Create state animation to control non-overview components. */
-    private void updateNonOverviewAnim(LauncherState toState, AnimatorSetBuilder builder,
-            @LauncherStateManager.AnimationComponents int animComponents) {
-        builder.addFlag(FLAG_DONT_ANIMATE_OVERVIEW);
-        long accuracy = (long) (Math.max(mXRange, mYRange) * 2);
-        mNonOverviewAnim = mLauncher.getStateManager().createAnimationToNewWorkspace(toState,
-                builder, accuracy, this::clearState, animComponents);
+    private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
+        config.duration = (long) (Math.max(mXRange, mYRange) * 2);
+        config.animFlags = config.animFlags | SKIP_OVERVIEW;
+        mNonOverviewAnim = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(toState, config)
+                .setOnCancelRunnable(this::clearState);
     }
 
     private void setupOverviewAnimators() {
         final LauncherState fromState = QUICK_SWITCH;
         final LauncherState toState = OVERVIEW;
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation = fromState
-                .getOverviewScaleAndTranslation(mLauncher);
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
-                .getOverviewScaleAndTranslation(mLauncher);
-        // Update RecentView's translationX to have it start offscreen.
-        LauncherRecentsView recentsView = mLauncher.getOverviewPanel();
-        float startScale = Utilities.mapRange(
-                SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
-                fromScaleAndTranslation.scale,
-                toScaleAndTranslation.scale);
-        fromScaleAndTranslation.translationX = recentsView.getOffscreenTranslationX(startScale);
 
         // Set RecentView's initial properties.
-        recentsView.setScaleX(fromScaleAndTranslation.scale);
-        recentsView.setScaleY(fromScaleAndTranslation.scale);
-        recentsView.setTranslationX(fromScaleAndTranslation.translationX);
-        recentsView.setTranslationY(fromScaleAndTranslation.translationY);
-        recentsView.setContentAlpha(1);
+        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
+        mRecentsView.setContentAlpha(1);
+        mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
+        mLauncher.getActionsView().getVisibilityAlpha().setValue(
+                (fromState.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1f : 0f);
 
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
         //   - RecentsView translationX
         //   - OverviewScrim
-        AnimatorSet xOverviewAnim = new AnimatorSet();
-        xOverviewAnim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X,
-                toScaleAndTranslation.translationX));
-        xOverviewAnim.play(ObjectAnimator.ofFloat(
-                mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
-                toState.getOverviewScrimAlpha(mLauncher)));
-        long xAccuracy = (long) (mXRange * 2);
-        xOverviewAnim.setDuration(xAccuracy);
-        mXOverviewAnim = AnimatorPlaybackController.wrap(xOverviewAnim, xAccuracy);
+        PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
+        xAnim.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1], LINEAR);
+        xAnim.setFloat(mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
+                toState.getOverviewScrimAlpha(mLauncher), LINEAR);
+        mXOverviewAnim = xAnim.createPlaybackController();
         mXOverviewAnim.dispatchOnStart();
 
         // As we drag up, animate the following properties:
-        //   - RecentsView translationY
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
-        AnimatorSet yAnimation = new AnimatorSet();
-        Animator translateYAnim = ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_Y,
-                toScaleAndTranslation.translationY);
-        Animator scaleAnim = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
-                toScaleAndTranslation.scale);
-        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS,
-                fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
-        scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        yAnimation.play(translateYAnim);
-        yAnimation.play(scaleAnim);
-        yAnimation.play(fullscreenProgressAnim);
-        long yAccuracy = (long) (mYRange * 2);
-        yAnimation.setDuration(yAccuracy);
-        mYOverviewAnim = AnimatorPlaybackController.wrap(yAnimation, yAccuracy);
+        PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
+        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+                toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
+        mYOverviewAnim = yAnim.createPlaybackController();
         mYOverviewAnim.dispatchOnStart();
     }
 
@@ -303,7 +295,7 @@
         // home screen elements will appear in the shelf on motion pause.
         mMotionPauseDetector.setDisallowPause(mIsHomeScreenVisible
                 || -displacement.y < mMotionPauseMinDisplacement);
-        mMotionPauseDetector.addPosition(displacement.y, ev.getEventTime());
+        mMotionPauseDetector.addPosition(ev);
 
         if (mIsHomeScreenVisible) {
             // Cancel the shelf anim so it doesn't clobber mNonOverviewAnim.
@@ -328,8 +320,8 @@
         if (mMotionPauseDetector.isPaused() && noFling) {
             cancelAnimations();
 
-            Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
-                    INDEX_PAUSE_TO_OVERVIEW_ANIM);
+            Animator overviewAnim = mLauncher.createAtomicAnimationFactory()
+                    .createStateElementAnimation(INDEX_PAUSE_TO_OVERVIEW_ANIM);
             overviewAnim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -378,7 +370,7 @@
         xOverviewAnim.setFloatValues(startXProgress, endXProgress);
         xOverviewAnim.setDuration(xDuration)
                 .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
-        mXOverviewAnim.dispatchOnStartWithVelocity(endXProgress, velocity.x);
+        mXOverviewAnim.dispatchOnStart();
 
         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
 
@@ -399,14 +391,16 @@
         ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
         yOverviewAnim.setFloatValues(startYProgress, endYProgress);
         yOverviewAnim.setDuration(yDuration);
-        mYOverviewAnim.dispatchOnStartWithVelocity(endYProgress, velocity.y);
+        mYOverviewAnim.dispatchOnStart();
 
         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
         if (flingUpToNormal && !mIsHomeScreenVisible) {
             // We are flinging to home while workspace is invisible, run the same staggered
             // animation as from an app.
+            StateAnimationConfig config = new StateAnimationConfig();
             // Update mNonOverviewAnim to do nothing so it doesn't interfere.
-            updateNonOverviewAnim(targetState, new AnimatorSetBuilder(), 0 /* animComponents */);
+            config.animFlags = 0;
+            updateNonOverviewAnim(targetState, config);
             nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
 
             new StaggeredWorkspaceAnim(mLauncher, velocity.y, false /* animateOverviewScrim */)
@@ -421,8 +415,7 @@
             float startProgress = mNonOverviewAnim.getProgressFraction();
             float endProgress = canceled ? 0 : 1;
             nonOverviewAnim.setFloatValues(startProgress, endProgress);
-            mNonOverviewAnim.dispatchOnStartWithVelocity(endProgress,
-                    horizontalFling ? velocity.x : velocity.y);
+            mNonOverviewAnim.dispatchOnStart();
         }
 
         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
@@ -441,6 +434,13 @@
                 mStartState.containerType,
                 targetState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(LAUNCHER_STATE_HOME)
+                .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
+                .log(getLauncherAtomEvent(mStartState.containerType, targetState.containerType,
+                        targetState.ordinal > mStartState.ordinal
+                                ? LAUNCHER_UNKNOWN_SWIPEUP
+                                : LAUNCHER_UNKNOWN_SWIPEDOWN));
         mLauncher.getStateManager().goToState(targetState, false, this::clearState);
     }
 
@@ -469,5 +469,6 @@
         mYOverviewAnim = null;
         mIsHomeScreenVisible = true;
         mSwipeDetector.finishedScrolling();
+        mRecentsView.setOnEmptyMessageUpdatedListener(null);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 03862db..845699a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -19,9 +19,10 @@
 import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.isTouchOverHotseat;
 
 import android.view.MotionEvent;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
@@ -74,12 +75,12 @@
      * @param duration how long the animation should be
      * @return the animation
      */
-    PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
+    PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
         mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
         TaskView taskView = mRecentsView.getCurrentPageTaskView();
         if (taskView == null) {
             throw new IllegalStateException("There is no task view to animate to.");
         }
-        return mRecentsView.createTaskLauncherAnimation(taskView, duration);
+        return mRecentsView.createTaskLaunchAnimation(taskView, duration, interpolator);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 14216ff..c643858 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -17,17 +17,17 @@
 
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_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_Y;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -35,20 +35,17 @@
 
 import android.view.MotionEvent;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -58,7 +55,7 @@
  */
 public class QuickSwitchTouchController extends AbstractStateChangeTouchController {
 
-    private @Nullable TaskView mTaskToLaunch;
+    protected final RecentsView mOverviewPanel;
 
     public QuickSwitchTouchController(Launcher launcher) {
         this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
@@ -66,6 +63,7 @@
 
     protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         super(l, dir);
+        mOverviewPanel = l.getOverviewPanel();
     }
 
     @Override
@@ -84,7 +82,7 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+        int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
             return NORMAL;
         }
@@ -92,46 +90,44 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
-        super.onDragStart(start);
+    public void onDragStart(boolean start, float startDisplacement) {
+        super.onDragStart(start, startDisplacement);
         mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
-        mTaskToLaunch = mLauncher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
         ActivityManagerWrapper.getInstance()
-            .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     }
 
     @Override
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
-        mTaskToLaunch = null;
     }
 
     @Override
     protected float initCurrentAnimation(int animComponents) {
-        AnimatorSetBuilder animatorSetBuilder = new AnimatorSetBuilder();
-        setupInterpolators(animatorSetBuilder);
-        long accuracy = (long) (getShiftRange() * 2);
-        mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
-                animatorSetBuilder, accuracy, this::clearState, LauncherStateManager.ANIM_ALL);
-        mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
-            updateFullscreenProgress((Float) valueAnimator.getAnimatedValue());
-        });
+        StateAnimationConfig config = new StateAnimationConfig();
+        setupInterpolators(config);
+        config.duration = (long) (getShiftRange() * 2);
+        mCurrentAnimation = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, config)
+                .setOnCancelRunnable(this::clearState);
+        mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator ->
+                updateFullscreenProgress((Float) valueAnimator.getAnimatedValue()));
         return 1 / getShiftRange();
     }
 
-    private void setupInterpolators(AnimatorSetBuilder animatorSetBuilder) {
-        animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
-        animatorSetBuilder.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
+    private void setupInterpolators(StateAnimationConfig stateAnimationConfig) {
+        stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
+        stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
         if (SysUINavigationMode.getMode(mLauncher) == Mode.NO_BUTTON) {
             // Overview lives to the left of workspace, so translate down later than over
-            animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
-            animatorSetBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
-            animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
-            animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
-            animatorSetBuilder.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+            stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
+            stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
+            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
+            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
+            stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
         } else {
-            animatorSetBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
-            animatorSetBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, LINEAR);
+            stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
+            stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, LINEAR);
         }
     }
 
@@ -142,13 +138,15 @@
     }
 
     private void updateFullscreenProgress(float progress) {
-        if (mTaskToLaunch != null) {
-            mTaskToLaunch.setFullscreenProgress(progress);
-            int sysuiFlags = progress > UPDATE_SYSUI_FLAGS_THRESHOLD
-                    ? mTaskToLaunch.getThumbnail().getSysUiStatusNavFlags()
-                    : 0;
-            mLauncher.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
+        mOverviewPanel.setFullscreenProgress(progress);
+        int sysuiFlags = 0;
+        if (progress > UPDATE_SYSUI_FLAGS_THRESHOLD) {
+            TaskView tv = mOverviewPanel.getTaskViewAt(0);
+            if (tv != null) {
+                sysuiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+            }
         }
+        mLauncher.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index ad02de1..0ee5d04 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -16,18 +16,15 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.view.MotionEvent;
+import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -35,11 +32,12 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SysUINavigationMode;
@@ -60,6 +58,7 @@
     private final SingleAxisSwipeDetector mDetector;
     private final RecentsView mRecentsView;
     private final int[] mTempCords = new int[2];
+    private final boolean mIsRtl;
 
     private PendingAnimation mPendingAnimation;
     private AnimatorPlaybackController mCurrentAnimation;
@@ -77,11 +76,17 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
-        mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL);
+        mIsRtl = Utilities.isRtl(activity.getResources());
+        SingleAxisSwipeDetector.Direction dir =
+            mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
+        mDetector = new SingleAxisSwipeDetector(activity, this, dir);
     }
 
     private boolean canInterceptTouch() {
         if (mCurrentAnimation != null) {
+            mCurrentAnimation.forceFinishIfCloseToEnd();
+        }
+        if (mCurrentAnimation != null) {
             // If we are already animating from a previous state, we can intercept.
             return true;
         }
@@ -93,6 +98,9 @@
 
     protected abstract boolean isRecentsInteractive();
 
+    /** Is recents view showing a single task in a modal way. */
+    protected abstract boolean isRecentsModal();
+
     protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
     }
 
@@ -105,6 +113,10 @@
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
+                && mCurrentAnimation == null) {
+            clearState();
+        }
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = !canInterceptTouch();
             if (mNoIntercept) {
@@ -123,8 +135,14 @@
 
                 for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
                     TaskView view = mRecentsView.getTaskViewAt(i);
+
                     if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
                             .isEventOverView(view, ev)) {
+                        // Disable swiping up and down if the task overlay is modal.
+                        if (isRecentsModal()) {
+                            mTaskBeingDragged = null;
+                            break;
+                        }
                         mTaskBeingDragged = view;
                         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
                             // Don't allow swipe down to open if we don't support swipe up
@@ -181,30 +199,35 @@
             mPendingAnimation = null;
         }
 
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         mCurrentAnimationIsGoingUp = goingUp;
         BaseDragLayer dl = mActivity.getDragLayer();
-        long maxDuration = (long) (2 * dl.getHeight());
-
+        final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
+        long maxDuration = 2 * secondaryLayerDimension;
+        int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
+        int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
         if (goingUp) {
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                     true /* animateTaskView */, true /* removeTask */, maxDuration);
 
-            mEndDisplacement = -mTaskBeingDragged.getHeight();
+            mEndDisplacement = -secondaryTaskDimension;
         } else {
-            mPendingAnimation = mRecentsView.createTaskLauncherAnimation(
-                    mTaskBeingDragged, maxDuration);
-            mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
+            mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
+                    mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
 
-            mTempCords[1] = mTaskBeingDragged.getHeight();
-            dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
-            mEndDisplacement = dl.getHeight() - mTempCords[1];
+            // Since the thumbnail is what is filling the screen, based the end displacement on it.
+            View thumbnailView = mTaskBeingDragged.getThumbnail();
+            mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
+            dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
+            mEndDisplacement = secondaryLayerDimension - mTempCords[1];
         }
+        mEndDisplacement *= verticalFactor;
 
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setOnCancelRunnable(null);
         }
-        mCurrentAnimation = AnimatorPlaybackController
-                .wrap(mPendingAnimation.anim, maxDuration, this::clearState);
+        mCurrentAnimation = mPendingAnimation.createPlaybackController()
+                .setOnCancelRunnable(this::clearState);
         onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
@@ -212,9 +235,10 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         if (mCurrentAnimation == null) {
-            reInitAnimationController(mDetector.wasInitialTouchPositive());
+            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
             mDisplacementShift = 0;
         } else {
             mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
@@ -225,9 +249,10 @@
 
     @Override
     public boolean onDrag(float displacement) {
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         float totalDisplacement = displacement + mDisplacementShift;
-        boolean isGoingUp =
-                totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
+        boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
+                orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
         if (isGoingUp != mCurrentAnimationIsGoingUp) {
             reInitAnimationController(isGoingUp);
             mFlingBlockCheck.blockFling();
@@ -254,11 +279,12 @@
         if (blockedFling) {
             fling = false;
         }
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
         if (fling) {
             logAction = Touch.FLING;
-            boolean goingUp = velocity < 0;
+            boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
             goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
         } else {
             logAction = Touch.SWIPE;
@@ -270,26 +296,16 @@
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
         }
 
-        float nextFrameProgress = Utilities.boundToRange(progress
-                + velocity * getSingleFrameMs(mActivity) / Math.abs(mEndDisplacement), 0f, 1f);
-
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
-        anim.setDuration(animationDuration);
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            anim.addUpdateListener(valueAnimator -> {
+            mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
                 if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
                     mRecentsView.redrawLiveTile(true);
                 }
             });
         }
-        if (QUICKSTEP_SPRINGS.get()) {
-            mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
-        }
-        anim.start();
+        mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
+                velocity, mEndDisplacement, animationDuration);
     }
 
     private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index ad90e16..de83caf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -15,50 +15,55 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
 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.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.graphics.Rect;
 import android.util.Log;
-import android.view.View;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-import com.android.systemui.shared.system.TransactionCompat;
 
 /**
- * Provider for the atomic remote window animation from the app to the overview.
+ * Provider for the atomic (for 3-button mode) remote window animation from the app to the overview.
  *
  * @param <T> activity that contains the overview
  */
-final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> implements
+final class AppToOverviewAnimationProvider<T extends StatefulActivity<?>> extends
         RemoteAnimationProvider {
 
     private static final long RECENTS_LAUNCH_DURATION = 250;
     private static final String TAG = "AppToOverviewAnimationProvider";
 
-    private final ActivityControlHelper<T> mHelper;
+    private final BaseActivityInterface<?, T> mActivityInterface;
     // The id of the currently running task that is transitioning to overview.
     private final int mTargetTaskId;
+    private final RecentsAnimationDeviceState mDeviceState;
 
     private T mActivity;
     private RecentsView mRecentsView;
 
-    AppToOverviewAnimationProvider(ActivityControlHelper<T> helper, int targetTaskId) {
-        mHelper = helper;
+    AppToOverviewAnimationProvider(
+            BaseActivityInterface<?, T> activityInterface, int targetTaskId,
+            RecentsAnimationDeviceState deviceState) {
+        mActivityInterface = activityInterface;
         mTargetTaskId = targetTaskId;
+        mDeviceState = deviceState;
     }
 
     /**
@@ -70,17 +75,13 @@
     boolean onActivityReady(T activity, Boolean wasVisible) {
         activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
         AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
-        ActivityControlHelper.AnimationFactory factory =
-                mHelper.prepareRecentsUI(activity, wasVisible,
-                false /* animate activity */, (controller) -> {
+        BaseActivityInterface.AnimationFactory factory = mActivityInterface.prepareRecentsUI(
+                mDeviceState,
+                wasVisible, (controller) -> {
                     controller.dispatchOnStart();
-                    ValueAnimator anim = controller.getAnimationPlayer()
-                            .setDuration(RECENTS_LAUNCH_DURATION);
-                    anim.setInterpolator(FAST_OUT_SLOW_IN);
-                    anim.start();
+                    controller.getAnimationPlayer().end();
                 });
-        factory.onRemoteAnimationReceived(null);
-        factory.createActivityController(RECENTS_LAUNCH_DURATION);
+        factory.createActivityInterface(RECENTS_LAUNCH_DURATION);
         factory.setRecentsAttachedToAppWindow(true, false);
         mActivity = activity;
         mRecentsView = mActivity.getOverviewPanel();
@@ -90,82 +91,75 @@
     /**
      * Create remote window animation from the currently running app to the overview panel.
      *
-     * @param targetCompats the target apps
+     * @param appTargets the target apps
      * @return animation from app to overview
      */
     @Override
-    public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
-        if (mRecentsView != null) {
-            mRecentsView.setRunningTaskIconScaledDown(true);
-        }
-        AnimatorSet anim = new AnimatorSet();
-        anim.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                mHelper.onSwipeUpToRecentsComplete(mActivity);
-                if (mRecentsView != null) {
-                    mRecentsView.animateUpRunningTaskIconScale();
-                }
-            }
-        });
+    public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets) {
+        PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
         if (mActivity == null) {
             Log.e(TAG, "Animation created, before activity");
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION));
-            return anim;
+            return pa.buildAnim();
         }
 
-        RemoteAnimationTargetSet targetSet =
-                new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING);
-
-        // Use the top closing app to determine the insets for the animation
-        RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mTargetTaskId);
-        if (runningTaskTarget == null) {
-            Log.e(TAG, "No closing app");
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION));
-            return anim;
-        }
-
-        final ClipAnimationHelper clipHelper = new ClipAnimationHelper(mActivity);
-
-        // At this point, the activity is already started and laid-out. Get the home-bounds
-        // relative to the screen using the rootView of the activity.
-        int loc[] = new int[2];
-        View rootView = mActivity.getRootView();
-        rootView.getLocationOnScreen(loc);
-        Rect homeBounds = new Rect(loc[0], loc[1],
-                loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight());
-        clipHelper.updateSource(homeBounds, runningTaskTarget);
-
-        Rect targetRect = new Rect();
-        mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, targetRect);
-        clipHelper.updateTargetRect(targetRect);
-        clipHelper.prepareAnimation(mActivity.getDeviceProfile(), false /* isOpening */);
-
-        ClipAnimationHelper.TransformParams params = new ClipAnimationHelper.TransformParams()
-                .setSyncTransactionApplier(new SyncRtSurfaceTransactionApplierCompat(rootView));
-        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
-        valueAnimator.setDuration(RECENTS_LAUNCH_DURATION);
-        valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
-        valueAnimator.addUpdateListener((v) -> {
-            params.setProgress((float) v.getAnimatedValue());
-            clipHelper.applyTransform(targetSet, params);
+        mRecentsView.setRunningTaskIconScaledDown(true);
+        pa.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mActivityInterface.onSwipeUpToRecentsComplete();
+                mRecentsView.animateUpRunningTaskIconScale();
+            }
         });
 
-        if (targetSet.isAnimatingHome()) {
-            // If we are animating home, fade in the opening targets
-            RemoteAnimationTargetSet openingSet =
-                    new RemoteAnimationTargetSet(targetCompats, MODE_OPENING);
-
-            TransactionCompat transaction = new TransactionCompat();
-            valueAnimator.addUpdateListener((v) -> {
-                for (RemoteAnimationTargetCompat app : openingSet.apps) {
-                    transaction.setAlpha(app.leash, (float) v.getAnimatedValue());
-                }
-                transaction.apply();
-            });
+        DepthController depthController = mActivityInterface.getDepthController();
+        if (depthController != null) {
+            pa.addFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(mActivity),
+                    OVERVIEW.getDepth(mActivity), TOUCH_RESPONSE_INTERPOLATOR);
         }
-        anim.play(valueAnimator);
-        return anim;
+
+        RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets,
+                wallpaperTargets, MODE_CLOSING);
+
+        // Use the top closing app to determine the insets for the animation
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mTargetTaskId);
+        if (runningTaskTarget == null) {
+            Log.e(TAG, "No closing app");
+            return pa.buildAnim();
+        }
+
+        TaskViewSimulator tsv = new TaskViewSimulator(mActivity, mRecentsView.getSizeStrategy());
+        tsv.setDp(mActivity.getDeviceProfile());
+        tsv.setPreview(runningTaskTarget);
+        tsv.setLayoutRotation(mRecentsView.getPagedViewOrientedState().getTouchRotation(),
+                mRecentsView.getPagedViewOrientedState().getDisplayRotation());
+
+        TransformParams params = new TransformParams()
+                .setTargetSet(targets)
+                .setSyncTransactionApplier(new SurfaceTransactionApplier(mActivity.getRootView()));
+
+        AnimatedFloat recentsAlpha = new AnimatedFloat(() -> { });
+        params.setBaseBuilderProxy((builder, app, p)
+                -> builder.withAlpha(recentsAlpha.value));
+
+        Interpolator taskInterpolator;
+        if (targets.isAnimatingHome()) {
+            params.setHomeBuilderProxy((builder, app, p) -> builder.withAlpha(1 - p.getProgress()));
+
+            taskInterpolator = TOUCH_RESPONSE_INTERPOLATOR;
+            pa.addFloat(recentsAlpha, AnimatedFloat.VALUE, 0, 1, TOUCH_RESPONSE_INTERPOLATOR);
+        } else {
+            // When animation from app to recents, the recents layer is drawn on top of the app. To
+            // prevent the overlap, we animate the task first and then quickly fade in the recents.
+            taskInterpolator = clampToProgress(TOUCH_RESPONSE_INTERPOLATOR, 0, 0.8f);
+            pa.addFloat(recentsAlpha, AnimatedFloat.VALUE, 0, 1,
+                    clampToProgress(TOUCH_RESPONSE_INTERPOLATOR, 0.8f, 1));
+        }
+
+        pa.addFloat(params, TransformParams.PROGRESS, 0, 1, taskInterpolator);
+        tsv.addAppToOverviewAnim(pa, taskInterpolator);
+        pa.addOnFrameCallback(() -> tsv.apply(params));
+        return pa.buildAnim();
     }
 
     /**
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index e5d2b41..a63f3a8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -15,171 +15,96 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
-import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 
-import android.animation.Animator;
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
+import android.util.Log;
 import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.Interpolator;
 
+import androidx.annotation.CallSuper;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
-import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.inputconsumers.InputConsumer;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 
+import java.util.ArrayList;
 import java.util.function.Consumer;
 
 /**
  * Base class for swipe up handler with some utility methods
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public abstract class BaseSwipeUpHandler<T extends BaseDraggingActivity, Q extends RecentsView>
-        implements SwipeAnimationListener {
+public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
+        extends SwipeUpAnimationLogic implements RecentsAnimationListener {
 
     private static final String TAG = "BaseSwipeUpHandler";
-    protected static final Rect TEMP_RECT = new Rect();
 
-    // Start resisting when swiping past this factor of mTransitionDragLength.
-    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
-    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
-
-    // The distance needed to drag to reach the task size in recents.
-    protected int mTransitionDragLength;
-    // How much further we can drag past recents, as a factor of mTransitionDragLength.
-    protected float mDragLengthFactor = 1;
-
-    protected final Context mContext;
-    protected final OverviewComponentObserver mOverviewComponentObserver;
-    protected final ActivityControlHelper<T> mActivityControlHelper;
-    protected final RecentsModel mRecentsModel;
-    protected final int mRunningTaskId;
-
-    protected final ClipAnimationHelper mClipAnimationHelper;
-    protected final TransformParams mTransformParams = new TransformParams();
-
-    protected final Mode mMode;
-
-    // Shift in the range of [0, 1].
-    // 0 => preview snapShot is completely visible, and hotseat is completely translated down
-    // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
-    // visible.
-    protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+    protected final BaseActivityInterface<?, T> mActivityInterface;
+    protected final InputConsumerController mInputConsumer;
 
     protected final ActivityInitListener mActivityInitListener;
-    protected final RecentsAnimationWrapper mRecentsAnimationWrapper;
+
+    protected RecentsAnimationController mRecentsAnimationController;
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
+
+    // Callbacks to be made once the recents animation starts
+    private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
 
     protected T mActivity;
     protected Q mRecentsView;
-    protected DeviceProfile mDp;
-    private final int mPageSpacing;
 
     protected Runnable mGestureEndCallback;
 
-    protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler();
     protected MultiStateCallback mStateCallback;
 
     protected boolean mCanceled;
-    protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
-    protected BaseSwipeUpHandler(Context context,
-            OverviewComponentObserver overviewComponentObserver,
-            RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
-        mContext = context;
-        mOverviewComponentObserver = overviewComponentObserver;
-        mActivityControlHelper = overviewComponentObserver.getActivityControlHelper();
-        mRecentsModel = recentsModel;
-        mActivityInitListener =
-                mActivityControlHelper.createActivityInitListener(this::onActivityInit);
-        mRunningTaskId = runningTaskId;
-        mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
-                this::createNewInputProxyHandler);
-        mMode = SysUINavigationMode.getMode(context);
+    private boolean mRecentsViewScrollLinked = false;
 
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-                .getDeviceProfile(mContext));
+    protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, InputConsumerController inputConsumer) {
+        super(context, deviceState, gestureState, new TransformParams());
+        mActivityInterface = gestureState.getActivityInterface();
+        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
+        mInputConsumer = inputConsumer;
     }
 
-    protected void setStateOnUiThread(int stateFlag) {
-        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
-            mStateCallback.setState(stateFlag);
-        } else {
-            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
-        }
+    /**
+     * To be called at the end of constructor of subclasses. This calls various methods which can
+     * depend on proper class initialization.
+     */
+    protected void initAfterSubclassConstructor() {
+        initTransitionEndpoints(
+                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
     }
 
     protected void performHapticFeedback() {
         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
-    public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
-        return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
-    }
-
-    @UiThread
-    public void updateDisplacement(float displacement) {
-        // We are moving in the negative x/y direction
-        displacement = -displacement;
-        float shift;
-        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
-            shift = mDragLengthFactor;
-        } else {
-            float translation = Math.max(displacement, 0);
-            shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-            if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
-                        * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
-            }
-        }
-
-        mCurrentShift.updateValue(shift);
+    public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
+        return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
     }
 
     public void setGestureEndCallback(Runnable gestureEndCallback) {
@@ -189,10 +114,10 @@
     public abstract Intent getLaunchIntent();
 
     protected void linkRecentsViewScroll() {
-        SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
+        SurfaceTransactionApplier.create(mRecentsView, applier -> {
             mTransformParams.setSyncTransactionApplier(applier);
-            mRecentsAnimationWrapper.runOnInit(() ->
-                    mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier));
+            runOnRecentsAnimationStart(() ->
+                    mRecentsAnimationTargets.addReleaseCheck(applier));
         });
 
         mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
@@ -200,11 +125,13 @@
                 updateFinalShift();
             }
         });
-        mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
-        mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
+        runOnRecentsAnimationStart(() ->
+                mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
+                        mRecentsAnimationTargets));
+        mRecentsViewScrollLinked = true;
     }
 
-    protected void startNewTask(int successStateFlag, Consumer<Boolean> resultCallback) {
+    protected void startNewTask(Consumer<Boolean> resultCallback) {
         // Launch the task user scrolled to (mRecentsView.getNextPage()).
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // We finish recents animation inside launchTask() when live tile is enabled.
@@ -212,96 +139,175 @@
                     true /* freezeTaskList */);
         } else {
             int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
-            mFinishingRecentsAnimationForNewTaskId = taskId;
-            mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
-                if (!mCanceled) {
-                    TaskView nextTask = mRecentsView.getTaskView(taskId);
-                    if (nextTask != null) {
-                        nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                                success -> {
-                                    resultCallback.accept(success);
-                                    if (!success) {
-                                        mActivityControlHelper.onLaunchTaskFailed(mActivity);
-                                        nextTask.notifyTaskLaunchFailed(TAG);
-                                    } else {
-                                        mActivityControlHelper.onLaunchTaskSuccess(mActivity);
+            if (!mCanceled) {
+                TaskView nextTask = mRecentsView.getTaskView(taskId);
+                if (nextTask != null) {
+                    mGestureState.updateLastStartedTaskId(taskId);
+                    boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
+                            .contains(taskId);
+                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+                            success -> {
+                                resultCallback.accept(success);
+                                if (success) {
+                                    if (hasTaskPreviouslyAppeared) {
+                                        onRestartPreviouslyAppearedTask();
                                     }
-                                }, mMainThreadHandler);
-                    }
-                    setStateOnUiThread(successStateFlag);
+                                } else {
+                                    mActivityInterface.onLaunchTaskFailed();
+                                    nextTask.notifyTaskLaunchFailed(TAG);
+                                    mRecentsAnimationController.finish(true /* toRecents */, null);
+                                }
+                            }, MAIN_EXECUTOR.getHandler());
                 }
-                mCanceled = false;
-                mFinishingRecentsAnimationForNewTaskId = -1;
-            });
+            }
+            mCanceled = false;
         }
-        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+    }
+
+    /**
+     * Called when we successfully startNewTask() on the task that was previously running. Normally
+     * we call resumeLastTask() when returning to the previously running task, but this handles a
+     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
+     * start A again to ensure it stays on top.
+     */
+    @CallSuper
+    protected void onRestartPreviouslyAppearedTask() {
+        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
+        // appeared.
+        if (mRecentsAnimationController != null) {
+            mRecentsAnimationController.finish(false, null);
+        }
+    }
+
+    /**
+     * Runs the given {@param action} if the recents animation has already started, or queues it to
+     * be run when it is next started.
+     */
+    protected void runOnRecentsAnimationStart(Runnable action) {
+        if (mRecentsAnimationTargets == null) {
+            mRecentsAnimationStartCallbacks.add(action);
+        } else {
+            action.run();
+        }
+    }
+
+    /**
+     * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
+     * @return whether the recents animation has started and there are valid app targets.
+     */
+    protected boolean hasTargets() {
+        return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
     }
 
     @Override
-    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
-        final Rect overviewStackBounds;
-        RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
+    public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
+            RecentsAnimationTargets targets) {
+        mRecentsAnimationController = recentsAnimationController;
+        mRecentsAnimationTargets = targets;
+        mTransformParams.setTargetSet(mRecentsAnimationTargets);
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
+                mGestureState.getRunningTaskId());
 
-        if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) {
-            overviewStackBounds = mActivityControlHelper
-                    .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget);
-            dp = dp.getMultiWindowProfile(mContext, new Point(
-                    overviewStackBounds.width(), overviewStackBounds.height()));
-        } else {
-            // If we are not in multi-window mode, home insets should be same as system insets.
-            dp = dp.copy(mContext);
-            overviewStackBounds = getStackBounds(dp);
-        }
-        dp.updateInsets(targetSet.homeContentInsets);
-        dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
-            mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+            mTaskViewSimulator.setPreview(runningTaskTarget);
         }
 
-        mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */);
-        initTransitionEndpoints(dp);
+        // Only initialize the device profile, if it has not been initialized before, as in some
+        // configurations targets.homeContentInsets may not be correct.
+        if (mActivity == null) {
+            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
+            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+                Rect overviewStackBounds = mActivityInterface
+                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+                dp = dp.getMultiWindowProfile(mContext,
+                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
+            } else {
+                // If we are not in multi-window mode, home insets should be same as system insets.
+                dp = dp.copy(mContext);
+            }
+            dp.updateInsets(targets.homeContentInsets);
+            dp.updateIsSeascape(mContext);
+            initTransitionEndpoints(dp);
+        }
 
-        mRecentsAnimationWrapper.setController(targetSet);
-    }
-
-    private Rect getStackBounds(DeviceProfile dp) {
-        if (mActivity != null) {
-            int loc[] = new int[2];
-            View rootView = mActivity.getRootView();
-            rootView.getLocationOnScreen(loc);
-            return new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(),
-                    loc[1] + rootView.getHeight());
-        } else {
-            return new Rect(0, 0, dp.widthPx, dp.heightPx);
+        // Notify when the animation starts
+        if (!mRecentsAnimationStartCallbacks.isEmpty()) {
+            for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
+                action.run();
+            }
+            mRecentsAnimationStartCallbacks.clear();
         }
     }
 
-    protected void initTransitionEndpoints(DeviceProfile dp) {
-        mDp = dp;
+    @Override
+    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
+        }
+    }
 
-        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
-                dp, mContext, TEMP_RECT);
-        if (!dp.isMultiWindowMode) {
-            // When updating the target rect, also update the home bounds since the location on
-            // screen of the launcher window may be stale (position is not updated until first
-            // traversal after the window is resized).  We only do this for non-multiwindow because
-            // we otherwise use the minimized home bounds provided by the system.
-            mClipAnimationHelper.updateHomeBounds(getStackBounds(dp));
+    @Override
+    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
         }
-        mClipAnimationHelper.updateTargetRect(TEMP_RECT);
-        if (mMode == Mode.NO_BUTTON) {
-            // We can drag all the way to the top of the screen.
-            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
+    }
+
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+        if (mRecentsAnimationController != null) {
+            if (handleTaskAppeared(appearedTaskTarget)) {
+                mRecentsAnimationController.finish(false /* toRecents */,
+                        null /* onFinishComplete */);
+                mActivityInterface.onLaunchTaskSuccess();
+                ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+            }
         }
     }
 
+    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
+    protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
+
+    /**
+     * @return The index of the TaskView in RecentsView whose taskId matches the task that will
+     * resume if we finish the controller.
+     */
+    protected int getLastAppearedTaskIndex() {
+        return mGestureState.getLastAppearedTaskId() != -1
+                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
+                : mRecentsView.getRunningTaskIndex();
+    }
+
+    /**
+     * @return Whether we are continuing a gesture that already landed on a new task,
+     * but before that task appeared.
+     */
+    protected boolean hasStartedNewTask() {
+        return mGestureState.getLastStartedTaskId() != -1;
+    }
+
     /**
      * Return true if the window should be translated horizontally if the recents view scrolls
      */
     protected abstract boolean moveWindowWithRecentsScroll();
 
-    protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome);
+    protected boolean onActivityInit(Boolean alreadyOnHome) {
+        T createdActivity = mActivityInterface.getCreatedActivity();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
+        }
+        if (createdActivity != null) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
+            }
+            initTransitionEndpoints(createdActivity.getDeviceProfile());
+        }
+        return true;
+    }
 
     /**
      * Called to create a input proxy for the running task
@@ -321,7 +327,7 @@
     public abstract void onMotionPauseChanged(boolean isPaused);
 
     @UiThread
-    public void onGestureStarted() { }
+    public void onGestureStarted(boolean isLikelyToStartNewTask) { }
 
     @UiThread
     public abstract void onGestureCancelled();
@@ -329,165 +335,51 @@
     @UiThread
     public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
 
-    public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState);
+    public abstract void onConsumerAboutToBeSwitched();
 
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
 
-    public void initWhenReady() {
+    /**
+     * Registers a callback to run when the activity is ready.
+     * @param intent The intent that will be used to start the activity if it doesn't exist already.
+     */
+    public void initWhenReady(Intent intent) {
         // Preload the plan
-        mRecentsModel.getTasks(null);
+        RecentsModel.INSTANCE.get(mContext).getTasks(null);
 
-        mActivityInitListener.register();
+        mActivityInitListener.register(intent);
     }
 
     /**
-     * Applies the transform on the recents animation without any additional null checks
+     * Applies the transform on the recents animation
      */
-    protected void applyTransformUnchecked() {
-        float shift = mCurrentShift.value;
-        float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
-        float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
-                mClipAnimationHelper.getTargetRect().width());
-        mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale);
-        mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
-                mTransformParams);
+    protected void applyWindowTransform() {
+        if (mWindowTransitionController != null) {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
+        }
+        if (mRecentsAnimationTargets != null) {
+            if (mRecentsViewScrollLinked) {
+                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+            }
+            mTaskViewSimulator.apply(mTransformParams);
+        }
     }
 
-    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
-        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing;
-        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
-        return TaskView.getCurveScaleForInterpolation(interpolation);
-    }
-
-    /**
-     * Creates an animation that transforms the current app window into the home app.
-     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
-     * @param homeAnimationFactory The home animation factory.
-     */
+    @Override
     protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
-        final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
-        final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
-                mTransformParams.setProgress(startProgress), false /* launcherOnTop */));
-        final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-
-        final View floatingView = homeAnimationFactory.getFloatingView();
-        final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
-        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources());
-        if (isFloatingIconView) {
-            FloatingIconView fiv = (FloatingIconView) floatingView;
-            anim.addAnimatorListener(fiv);
-            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+        RectFSpringAnim anim =
+                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
+        if (mRecentsAnimationTargets != null) {
+            mRecentsAnimationTargets.addReleaseCheck(anim);
         }
-
-        AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
-
-        // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
-        // rounding at the end of the animation.
-        float startRadius = mClipAnimationHelper.getCurrentCornerRadius();
-        float endRadius = startRect.width() / 6f;
-
-        float startTransformProgress = mTransformParams.getProgress();
-        float endTransformProgress = 1;
-
-        // We want the window alpha to be 0 once this threshold is met, so that the
-        // FolderIconView can be seen morphing into the icon shape.
-        final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
-        anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() {
-
-            // Alpha interpolates between [1, 0] between progress values [start, end]
-            final float start = 0f;
-            final float end = 0.85f;
-
-            private float getWindowAlpha(float progress) {
-                if (progress <= start) {
-                    return 1f;
-                }
-                if (progress >= end) {
-                    return 0f;
-                }
-                return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
-            }
-
-            @Override
-            public void onUpdate(RectF currentRect, float progress) {
-                homeAnim.setPlayFraction(progress);
-
-                mTransformParams.setProgress(
-                        Utilities.mapRange(progress, startTransformProgress, endTransformProgress))
-                        .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress));
-                if (isFloatingIconView) {
-                    mTransformParams.setCornerRadius(endRadius * progress + startRadius
-                            * (1f - progress));
-                }
-                mClipAnimationHelper.applyTransform(targetSet, mTransformParams,
-                        false /* launcherOnTop */);
-
-                if (isFloatingIconView) {
-                    ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
-                            windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(),
-                            false);
-                }
-            }
-
-            @Override
-            public void onCancel() {
-                if (isFloatingIconView) {
-                    ((FloatingIconView) floatingView).fastFinish();
-                }
-            }
-        });
-        anim.addAnimatorListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                homeAnim.dispatchOnStart();
-            }
-
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                homeAnim.getAnimationPlayer().end();
-            }
-        });
         return anim;
     }
 
     public interface Factory {
 
-        BaseSwipeUpHandler newHandler(RunningTaskInfo runningTask,
-                long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask);
-    }
-
-    protected interface RunningWindowAnim {
-        void end();
-
-        void cancel();
-
-        static RunningWindowAnim wrap(Animator animator) {
-            return new RunningWindowAnim() {
-                @Override
-                public void end() {
-                    animator.end();
-                }
-
-                @Override
-                public void cancel() {
-                    animator.cancel();
-                }
-            };
-        }
-
-        static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
-            return new RunningWindowAnim() {
-                @Override
-                public void end() {
-                    rectFSpringAnim.end();
-                }
-
-                @Override
-                public void cancel() {
-                    rectFSpringAnim.cancel();
-                }
-            };
-        }
+        BaseSwipeUpHandler newHandler(
+                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
new file mode 100644
index 0000000..6c4c5d3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -0,0 +1,1335 @@
+/*
+ * 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.quickstep;
+
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
+import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+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;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
+import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
+import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
+import static com.android.quickstep.GestureState.STATE_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.SysUINavigationMode.Mode.TWO_BUTTONS;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+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.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.View;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.WindowInsets;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.BaseActivityInterface.AnimationFactory;
+import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+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;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.LatencyTrackerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TaskInfoCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+
+/**
+ * Handles the navigation gestures when Launcher is the default home activity.
+ * TODO: Merge this with BaseSwipeUpHandler
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public abstract class BaseSwipeUpHandlerV2<T extends StatefulActivity<?>, Q extends RecentsView>
+        extends BaseSwipeUpHandler<T, Q> implements OnApplyWindowInsetsListener {
+    private static final String TAG = BaseSwipeUpHandlerV2.class.getSimpleName();
+
+    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
+
+    private static int getFlagForIndex(int index, String name) {
+        if (DEBUG_STATES) {
+            STATE_NAMES[index] = name;
+        }
+        return 1 << index;
+    }
+
+    // Launcher UI related states
+    protected static final int STATE_LAUNCHER_PRESENT =
+            getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
+    protected static final int STATE_LAUNCHER_STARTED =
+            getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
+    protected static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
+
+    // Internal initialization states
+    private static final int STATE_APP_CONTROLLER_RECEIVED =
+            getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED");
+
+    // Interaction finish states
+    private static final int STATE_SCALED_CONTROLLER_HOME =
+            getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
+    private static final int STATE_SCALED_CONTROLLER_RECENTS =
+            getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
+
+    protected static final int STATE_HANDLER_INVALIDATED =
+            getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
+    private static final int STATE_GESTURE_STARTED =
+            getFlagForIndex(7, "STATE_GESTURE_STARTED");
+    private static final int STATE_GESTURE_CANCELLED =
+            getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
+    private static final int STATE_GESTURE_COMPLETED =
+            getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
+
+    private static final int STATE_CAPTURE_SCREENSHOT =
+            getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
+    protected static final int STATE_SCREENSHOT_CAPTURED =
+            getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
+    private static final int STATE_SCREENSHOT_VIEW_SHOWN =
+            getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
+
+    private static final int STATE_RESUME_LAST_TASK =
+            getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
+    private static final int STATE_START_NEW_TASK =
+            getFlagForIndex(14, "STATE_START_NEW_TASK");
+    private static final int STATE_CURRENT_TASK_FINISHED =
+            getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
+
+    private static final int LAUNCHER_UI_STATES =
+            STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
+
+    public static final long MAX_SWIPE_DURATION = 350;
+    public static final long MIN_OVERSHOOT_DURATION = 120;
+
+    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
+    private static final float SWIPE_DURATION_MULTIPLIER =
+            Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
+    private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
+
+    public static final long RECENTS_ATTACH_DURATION = 300;
+
+    /**
+     * Used as the page index for logging when we return to the last task at the end of the gesture.
+     */
+    private static final int LOG_NO_OP_PAGE_INDEX = -1;
+
+    protected final TaskAnimationManager mTaskAnimationManager;
+
+    // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
+    private RunningWindowAnim mRunningWindowAnim;
+    private boolean mIsShelfPeeking;
+
+    private boolean mContinuingLastGesture;
+
+    private ThumbnailData mTaskSnapshot;
+
+    // Used to control launcher components throughout the swipe gesture.
+    private AnimatorPlaybackController mLauncherTransitionController;
+    private boolean mHasLauncherTransitionControllerStarted;
+
+    private AnimationFactory mAnimationFactory = (t) -> { };
+
+    private boolean mWasLauncherAlreadyVisible;
+
+    private boolean mPassedOverviewThreshold;
+    private boolean mGestureStarted;
+    private int mLogAction = Touch.SWIPE;
+    private int mLogDirection = Direction.UP;
+    private PointF mDownPos;
+    private boolean mIsLikelyToStartNewTask;
+
+    private final long mTouchTimeMs;
+    private long mLauncherFrameDrawnTime;
+
+    private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
+
+    public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState,
+            long touchTimeMs, boolean continuingLastGesture,
+            InputConsumerController inputConsumer) {
+        super(context, deviceState, gestureState, inputConsumer);
+        mTaskAnimationManager = taskAnimationManager;
+        mTouchTimeMs = touchTimeMs;
+        mContinuingLastGesture = continuingLastGesture;
+
+        initAfterSubclassConstructor();
+        initStateCallbacks();
+    }
+
+    private void initStateCallbacks() {
+        mStateCallback = new MultiStateCallback(STATE_NAMES);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
+                this::onLauncherPresentAndGestureStarted);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
+                this::initializeLauncherAnimationController);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
+                this::launcherFrameDrawn);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
+                        | STATE_GESTURE_CANCELLED,
+                this::resetStateForAnimationCancel);
+
+        mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
+                this::resumeLastTask);
+        mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
+                this::startNewTask);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+                        | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
+                this::switchToScreenshot);
+
+        mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+                        | STATE_SCALED_CONTROLLER_RECENTS,
+                this::finishCurrentTransitionToRecents);
+
+        mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+                        | STATE_SCALED_CONTROLLER_HOME,
+                this::finishCurrentTransitionToHome);
+        mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+                this::reset);
+
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+                        | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
+                        | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
+                        | STATE_GESTURE_STARTED,
+                this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
+
+        mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED,
+                this::continueComputingRecentsScrollIfNecessary);
+        mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED
+                        | STATE_RECENTS_SCROLLING_FINISHED,
+                this::onSettledOnEndTarget);
+
+        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
+        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+                this::invalidateHandlerWithLauncher);
+        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
+                this::notifyTransitionCancelled);
+
+        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
+                () -> mDeviceState.onEndTargetCalculated(mGestureState.getEndTarget(),
+                        mActivityInterface));
+
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
+                            | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
+                    (b) -> mRecentsView.setRunningTaskHidden(!b));
+        }
+    }
+
+    @Override
+    protected boolean onActivityInit(Boolean alreadyOnHome) {
+        super.onActivityInit(alreadyOnHome);
+        final T activity = mActivityInterface.getCreatedActivity();
+        if (mActivity == activity) {
+            return true;
+        }
+
+        if (mActivity != null) {
+            // The launcher may have been recreated as a result of device rotation.
+            int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
+            initStateCallbacks();
+            mStateCallback.setState(oldState);
+        }
+        mWasLauncherAlreadyVisible = alreadyOnHome;
+        mActivity = activity;
+        // Override the visibility of the activity until the gesture actually starts and we swipe
+        // up, or until we transition home and the home animation is composed
+        if (alreadyOnHome) {
+            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+        } else {
+            mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+        }
+
+        mRecentsView = activity.getOverviewPanel();
+        mRecentsView.setOnPageTransitionEndCallback(null);
+        addLiveTileOverlay();
+
+        mStateCallback.setState(STATE_LAUNCHER_PRESENT);
+        if (alreadyOnHome) {
+            onLauncherStart();
+        } else {
+            activity.runOnceOnStart(this::onLauncherStart);
+        }
+
+        setupRecentsViewUi();
+
+        if (mDeviceState.getNavMode() == TWO_BUTTONS) {
+            // If the device is in two button mode, swiping up will show overview with predictions
+            // so we need to kick off switching to the overview predictions as soon as possible
+            mActivityInterface.updateOverviewPredictionState();
+        }
+        linkRecentsViewScroll();
+
+        return true;
+    }
+
+    @Override
+    protected boolean moveWindowWithRecentsScroll() {
+        return mGestureState.getEndTarget() != HOME;
+    }
+
+    private void onLauncherStart() {
+        final T activity = mActivityInterface.getCreatedActivity();
+        if (mActivity != activity) {
+            return;
+        }
+        if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+            return;
+        }
+        mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
+
+        // If we've already ended the gesture and are going home, don't prepare recents UI,
+        // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
+        if (mGestureState.getEndTarget() != HOME) {
+            Runnable initAnimFactory = () -> {
+                mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState,
+                        mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
+                maybeUpdateRecentsAttachedState(false /* animate */);
+            };
+            if (mWasLauncherAlreadyVisible) {
+                // Launcher is visible, but might be about to stop. Thus, if we prepare recents
+                // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
+                // wait until the next gesture (and possibly launcher) starts.
+                mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, initAnimFactory);
+            } else {
+                initAnimFactory.run();
+            }
+        }
+        AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
+                AbstractFloatingView.TYPE_LISTENER);
+
+        if (mWasLauncherAlreadyVisible) {
+            mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+        } else {
+            Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
+            View dragLayer = activity.getDragLayer();
+            dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
+                boolean mHandled = false;
+
+                @Override
+                public void onDraw() {
+                    if (mHandled) {
+                        return;
+                    }
+                    mHandled = true;
+
+                    TraceHelper.INSTANCE.endSection(traceToken);
+                    dragLayer.post(() ->
+                            dragLayer.getViewTreeObserver().removeOnDrawListener(this));
+                    if (activity != mActivity) {
+                        return;
+                    }
+
+                    mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+                }
+            });
+        }
+
+        activity.getRootView().setOnApplyWindowInsetsListener(this);
+        mStateCallback.setState(STATE_LAUNCHER_STARTED);
+    }
+
+    private void onLauncherPresentAndGestureStarted() {
+        // Re-setup the recents UI when gesture starts, as the state could have been changed during
+        // that time by a previous window transition.
+        setupRecentsViewUi();
+
+        // For the duration of the gesture, in cases where an activity is launched while the
+        // activity is not yet resumed, finish the animation to ensure we get resumed
+        mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
+                mOnDeferredActivityLaunch);
+
+        notifyGestureStartedAsync();
+    }
+
+    private void onDeferredActivityLaunch() {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mActivityInterface.switchRunningTaskViewToScreenshot(
+                    null, () -> {
+                        mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
+                    });
+        } else {
+            mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
+        }
+    }
+
+    private void setupRecentsViewUi() {
+        if (mContinuingLastGesture) {
+            updateSysUiFlags(mCurrentShift.value);
+            return;
+        }
+        notifyGestureAnimationStartToRecents();
+    }
+
+    protected void notifyGestureAnimationStartToRecents() {
+        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
+    }
+
+    private void launcherFrameDrawn() {
+        mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
+    }
+
+    private void initializeLauncherAnimationController() {
+        buildAnimationController();
+
+        Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
+                TraceHelper.FLAG_IGNORE_BINDERS);
+        // Only used in debug builds
+        if (LatencyTrackerCompat.isEnabled(mContext)) {
+            LatencyTrackerCompat.logToggleRecents(
+                    (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+        }
+        TraceHelper.INSTANCE.endSection(traceToken);
+
+        // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
+        // high-res thumbnail loader here once we are sure that we will end up in an overview state
+        RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
+    }
+
+    @Override
+    public void onMotionPauseChanged(boolean isPaused) {
+        setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
+
+        if (mDeviceState.isFullyGesturalNavMode() && isPaused) {
+            // In fully gestural nav mode, switch to overview predictions once the user has paused
+            // (this is a no-op if the predictions are already in that state)
+            mActivityInterface.updateOverviewPredictionState();
+        }
+    }
+
+    public void maybeUpdateRecentsAttachedState() {
+        maybeUpdateRecentsAttachedState(true /* animate */);
+    }
+
+    /**
+     * Determines whether to show or hide RecentsView. The window is always
+     * synchronized with its corresponding TaskView in RecentsView, so if
+     * RecentsView is shown, it will appear to be attached to the window.
+     *
+     * Note this method has no effect unless the navigation mode is NO_BUTTON.
+     */
+    private void maybeUpdateRecentsAttachedState(boolean animate) {
+        if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
+            return;
+        }
+        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
+                ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                : null;
+        final boolean recentsAttachedToAppWindow;
+        if (mGestureState.getEndTarget() != null) {
+            recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
+        } else if (mContinuingLastGesture
+                && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
+            recentsAttachedToAppWindow = true;
+        } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
+            // The window is going away so make sure recents is always visible in this case.
+            recentsAttachedToAppWindow = true;
+        } else {
+            recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
+        }
+        mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+    }
+
+    @Override
+    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
+        setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
+    }
+
+    private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
+        if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
+            mIsLikelyToStartNewTask = isLikelyToStartNewTask;
+            maybeUpdateRecentsAttachedState(animate);
+        }
+    }
+
+    @UiThread
+    public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
+        mAnimationFactory.setShelfState(shelfState, interpolator, duration);
+        boolean wasShelfPeeking = mIsShelfPeeking;
+        mIsShelfPeeking = shelfState == PEEK;
+        if (mIsShelfPeeking != wasShelfPeeking) {
+            maybeUpdateRecentsAttachedState();
+        }
+        if (shelfState.shouldPreformHaptic) {
+            performHapticFeedback();
+        }
+    }
+
+    private void buildAnimationController() {
+        if (!canCreateNewOrUpdateExistingLauncherTransitionController()) {
+            return;
+        }
+        initTransitionEndpoints(mActivity.getDeviceProfile());
+        mAnimationFactory.createActivityInterface(mTransitionDragLength);
+    }
+
+    /**
+     * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
+     * (it has its own animation) or if we're already animating the current controller.
+     * @return Whether we can create the launcher controller or update its progress.
+     */
+    private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
+        return mGestureState.getEndTarget() != HOME && !mHasLauncherTransitionControllerStarted;
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
+        WindowInsets result = view.onApplyWindowInsets(windowInsets);
+        buildAnimationController();
+        return result;
+    }
+
+    private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
+        mLauncherTransitionController = anim;
+        mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
+        mLauncherTransitionController.dispatchOnStart();
+        updateLauncherTransitionProgress();
+    }
+
+    @Override
+    public Intent getLaunchIntent() {
+        return mGestureState.getOverviewIntent();
+    }
+
+    @Override
+    public void updateFinalShift() {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mRecentsAnimationTargets != null) {
+                LiveTileOverlay.INSTANCE.update(
+                        mTaskViewSimulator.getCurrentCropRect(),
+                        mTaskViewSimulator.getCurrentCornerRadius());
+            }
+        }
+
+        final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+        if (passed != mPassedOverviewThreshold) {
+            mPassedOverviewThreshold = passed;
+            if (!mDeviceState.isFullyGesturalNavMode()) {
+                performHapticFeedback();
+            }
+        }
+
+        updateSysUiFlags(mCurrentShift.value);
+        applyWindowTransform();
+        updateLauncherTransitionProgress();
+    }
+
+    private void updateLauncherTransitionProgress() {
+        if (mLauncherTransitionController == null
+                || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
+            return;
+        }
+        // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
+        // anyway. The controller mimics the drag length factor by applying it to its interpolators.
+        float progress = mCurrentShift.value / mDragLengthFactor;
+        mLauncherTransitionController.setPlayFraction(progress);
+    }
+
+    /**
+     * @param windowProgress 0 == app, 1 == overview
+     */
+    private void updateSysUiFlags(float windowProgress) {
+        if (mRecentsAnimationController != null && mRecentsView != null) {
+            TaskView runningTask = mRecentsView.getRunningTaskView();
+            TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
+            int centermostTaskFlags = centermostTask == null ? 0
+                    : centermostTask.getThumbnail().getSysUiStatusNavFlags();
+            boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
+            boolean quickswitchThresholdPassed = centermostTask != runningTask;
+
+            // We will handle the sysui flags based on the centermost task view.
+            mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
+                    ||  (quickswitchThresholdPassed && centermostTaskFlags != 0));
+            mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed);
+
+            int sysuiFlags = swipeUpThresholdPassed ? 0 : centermostTaskFlags;
+            mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
+        }
+    }
+
+    @Override
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
+        super.onRecentsAnimationStart(controller, targets);
+
+        // Only add the callback to enable the input consumer after we actually have the controller
+        mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
+                mRecentsAnimationController::enableInputConsumer);
+        mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
+
+        mPassedOverviewThreshold = false;
+    }
+
+    @Override
+    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+        ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
+        mActivityInitListener.unregister();
+        mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+
+        // Defer clearing the controller and the targets until after we've updated the state
+        super.onRecentsAnimationCanceled(thumbnailData);
+    }
+
+    @Override
+    public void onGestureStarted(boolean isLikelyToStartNewTask) {
+        notifyGestureStartedAsync();
+        setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
+        mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
+        mGestureStarted = true;
+    }
+
+    /**
+     * Notifies the launcher that the swipe gesture has started. This can be called multiple times.
+     */
+    @UiThread
+    private void notifyGestureStartedAsync() {
+        final T curActivity = mActivity;
+        if (curActivity != null) {
+            // Once the gesture starts, we can no longer transition home through the button, so
+            // reset the force override of the activity visibility
+            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+        }
+    }
+
+    /**
+     * Called as a result on ACTION_CANCEL to return the UI to the start state.
+     */
+    @Override
+    public void onGestureCancelled() {
+        updateDisplacement(0);
+        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+        mLogAction = Touch.SWIPE_NOOP;
+        handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
+    }
+
+    /**
+     * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
+     * @param velocity The x and y components of the velocity when the gesture ends.
+     * @param downPos The x and y value of where the gesture started.
+     */
+    @Override
+    public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
+        float flingThreshold = mContext.getResources()
+                .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+        boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
+        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+
+        mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
+        boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
+        if (isVelocityVertical) {
+            mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN;
+        } else {
+            mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
+        }
+        mDownPos = downPos;
+        handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
+    }
+
+    @Override
+    protected InputConsumer createNewInputProxyHandler() {
+        endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
+        endLauncherTransitionController();
+
+        StatefulActivity activity = mActivityInterface.getCreatedActivity();
+        return activity == null ? InputConsumer.NO_OP
+                : new OverviewInputConsumer(mGestureState, activity, null, true);
+    }
+
+    private void endRunningWindowAnim(boolean cancel) {
+        if (mRunningWindowAnim != null) {
+            if (cancel) {
+                mRunningWindowAnim.cancel();
+            } else {
+                mRunningWindowAnim.end();
+            }
+        }
+    }
+
+    private void onSettledOnEndTarget() {
+        switch (mGestureState.getEndTarget()) {
+            case HOME:
+                mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
+                // Notify swipe-to-home (recents animation) is finished
+                SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
+                break;
+            case RECENTS:
+                mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+                        | STATE_SCREENSHOT_VIEW_SHOWN);
+                break;
+            case NEW_TASK:
+                mStateCallback.setState(STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT);
+                break;
+            case LAST_TASK:
+                mStateCallback.setState(STATE_RESUME_LAST_TASK);
+                break;
+        }
+        ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + mGestureState.getEndTarget());
+    }
+
+    @Override
+    protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+        if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+            return false;
+        }
+        if (mStateCallback.hasStates(STATE_START_NEW_TASK)
+                && appearedTaskTarget.taskId == mGestureState.getLastStartedTaskId()) {
+            reset();
+            return true;
+        }
+        return false;
+    }
+
+    private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
+            boolean isCancel) {
+        final GestureEndTarget endTarget;
+        final boolean goingToNewTask;
+        if (mRecentsView != null) {
+            if (!hasTargets()) {
+                // If there are no running tasks, then we can assume that this is a continuation of
+                // the last gesture, but after the recents animation has finished
+                goingToNewTask = true;
+            } else {
+                final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+                final int taskToLaunch = mRecentsView.getNextPage();
+                goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
+            }
+        } else {
+            goingToNewTask = false;
+        }
+        final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+        if (!isFling) {
+            if (isCancel) {
+                endTarget = LAST_TASK;
+            } else if (mDeviceState.isFullyGesturalNavMode()) {
+                if (mIsShelfPeeking) {
+                    endTarget = RECENTS;
+                } else if (goingToNewTask) {
+                    endTarget = NEW_TASK;
+                } else {
+                    endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME;
+                }
+            } else {
+                endTarget = reachedOverviewThreshold && mGestureStarted
+                        ? RECENTS
+                        : goingToNewTask
+                                ? NEW_TASK
+                                : LAST_TASK;
+            }
+        } else {
+            // If swiping at a diagonal, base end target on the faster velocity.
+            boolean isSwipeUp = endVelocity < 0;
+            boolean willGoToNewTaskOnSwipeUp =
+                    goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
+
+            if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
+                endTarget = HOME;
+            } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !mIsShelfPeeking) {
+                // If swiping at a diagonal, base end target on the faster velocity.
+                endTarget = NEW_TASK;
+            } else if (isSwipeUp) {
+                endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp
+                        ? NEW_TASK : RECENTS;
+            } else {
+                endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
+            }
+        }
+
+        if (mDeviceState.isOverviewDisabled() && (endTarget == RECENTS || endTarget == LAST_TASK)) {
+            return LAST_TASK;
+        }
+        return endTarget;
+    }
+
+    @UiThread
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
+            boolean isCancel) {
+        PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
+        long duration = MAX_SWIPE_DURATION;
+        float currentShift = mCurrentShift.value;
+        final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
+                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 - velocityPxPerMs.y
+                    * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
+            float minFlingVelocity = mContext.getResources()
+                    .getDimension(R.dimen.quickstep_fling_min_velocity);
+            if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
+                if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
+                    Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
+                            startShift, endShift, endShift, endVelocity / 1000,
+                            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
+                    // which the user flings, so we scale the duration by a value near to the
+                    // derivative of the scroll interpolator at zero, ie. 2.
+                    long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
+                    duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+
+                    if (endTarget == RECENTS) {
+                        interpolator = OVERSHOOT_1_2;
+                    }
+                }
+            }
+        }
+
+        if (endTarget.isLauncher && mRecentsAnimationController != null) {
+            mRecentsAnimationController.enableInputProxy(mInputConsumer,
+                    this::createNewInputProxyHandler);
+        }
+
+        if (endTarget == HOME) {
+            setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+            duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
+        } else if (endTarget == RECENTS) {
+            LiveTileOverlay.INSTANCE.startIconAnimation();
+            if (mRecentsView != null) {
+                int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
+                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.
+                    mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
+                }
+                if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
+                    mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
+                }
+                duration = Math.max(duration, mRecentsView.getScroller().getDuration());
+            }
+            if (mDeviceState.isFullyGesturalNavMode()) {
+                setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
+            }
+        }
+
+        // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
+        // or resumeLastTask().
+        if (mRecentsView != null) {
+            mRecentsView.setOnPageTransitionEndCallback(
+                    () -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED));
+        } else {
+            mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
+        }
+
+        animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
+    }
+
+    private void doLogGesture(GestureEndTarget endTarget) {
+        DeviceProfile dp = mDp;
+        if (dp == null || mDownPos == null) {
+            // We probably never received an animation controller, skip logging.
+            return;
+        }
+
+        int pageIndex = endTarget == LAST_TASK
+                ? LOG_NO_OP_PAGE_INDEX
+                : mRecentsView.getNextPage();
+        UserEventDispatcher.newInstance(mContext).logStateChangeAction(
+                mLogAction, mLogDirection,
+                (int) mDownPos.x, (int) mDownPos.y,
+                ContainerType.NAVBAR, ContainerType.APP,
+                endTarget.containerType,
+                pageIndex);
+        StatsLogManager.EventEnum event;
+        switch (endTarget) {
+            case HOME:
+                event = LAUNCHER_HOME_GESTURE;
+                break;
+            case RECENTS:
+                event = LAUNCHER_OVERVIEW_GESTURE;
+                break;
+            case LAST_TASK:
+            case NEW_TASK:
+                event = (mLogDirection == Direction.LEFT)
+                        ? LAUNCHER_QUICKSWITCH_LEFT
+                        : LAUNCHER_QUICKSWITCH_RIGHT;
+                break;
+            default:
+                event = IGNORE;
+        }
+        StatsLogManager.newInstance(mContext).logger()
+                .withSrcState(LAUNCHER_STATE_BACKGROUND)
+                .withDstState(StatsLogManager.containerTypeToAtomState(endTarget.containerType))
+                .log(event);
+    }
+
+    /** Animates to the given progress, where 0 is the current app and 1 is overview. */
+    @UiThread
+    private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
+            GestureEndTarget target, PointF velocityPxPerMs) {
+        runOnRecentsAnimationStart(() -> animateToProgressInternal(start, end, duration,
+                interpolator, target, velocityPxPerMs));
+    }
+
+    protected abstract HomeAnimationFactory createHomeAnimationFactory(long duration);
+
+    private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() {
+        @Override
+        public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+                boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+            if (task.taskId == mGestureState.getRunningTaskId()
+                    && TaskInfoCompat.getActivityType(task) != ACTIVITY_TYPE_HOME) {
+                // Since this is an edge case, just cancel and relaunch with default activity
+                // options (since we don't know if there's an associated app icon to launch from)
+                endRunningWindowAnim(true /* cancel */);
+                ActivityManagerWrapper.getInstance().unregisterTaskStackListener(
+                        mActivityRestartListener);
+                ActivityManagerWrapper.getInstance().startActivityFromRecents(task.taskId, null);
+            }
+        }
+    };
+
+    @UiThread
+    private void animateToProgressInternal(float start, float end, long duration,
+            Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
+        // Set the state, but don't notify until the animation completes
+        mGestureState.setEndTarget(target, false /* isAtomic */);
+        maybeUpdateRecentsAttachedState();
+
+        // If we are transitioning to launcher, then listen for the activity to be restarted while
+        // the transition is in progress
+        if (mGestureState.getEndTarget().isLauncher) {
+            ActivityManagerWrapper.getInstance().registerTaskStackListener(
+                    mActivityRestartListener);
+        }
+
+        if (mGestureState.getEndTarget() == HOME) {
+            HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
+            RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+            windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    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
+                        // skip doing any future work here for the current gesture.
+                        return;
+                    }
+                    // Finalize the state and notify of the change
+                    mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+                }
+            });
+            getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
+            windowAnim.start(mContext, velocityPxPerMs);
+            homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
+            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+            mLauncherTransitionController = null;
+        } else {
+            ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
+            windowAnim.setDuration(duration).setInterpolator(interpolator);
+            windowAnim.addUpdateListener(valueAnimator -> {
+                computeRecentsScrollIfInvisible();
+            });
+            windowAnim.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    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
+                        // skip doing any future work here for the current gesture.
+                        return;
+                    }
+                    if (mRecentsView != null) {
+                        int taskToLaunch = mRecentsView.getNextPage();
+                        int runningTask = getLastAppearedTaskIndex();
+                        boolean hasStartedNewTask = hasStartedNewTask();
+                        if (target == NEW_TASK && taskToLaunch == runningTask
+                                && !hasStartedNewTask) {
+                            // We are about to launch the current running task, so use LAST_TASK
+                            // state instead of NEW_TASK. This could happen, for example, if our
+                            // scroll is aborted after we determined the target to be NEW_TASK.
+                            mGestureState.setEndTarget(LAST_TASK);
+                        } else if (target == LAST_TASK && hasStartedNewTask) {
+                            // We are about to re-launch the previously running task, but we can't
+                            // just finish the controller like we normally would because that would
+                            // instead resume the last task that appeared, and not ensure that this
+                            // task is restored to the top. To address this, re-launch the task as
+                            // if it were a new task.
+                            mGestureState.setEndTarget(NEW_TASK);
+                        }
+                    }
+                    mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+                }
+            });
+            windowAnim.start();
+            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+        }
+        // Always play the entire launcher animation when going home, since it is separate from
+        // the animation that has been controlled thus far.
+        if (mGestureState.getEndTarget() == HOME) {
+            start = 0;
+        }
+
+        // We want to use the same interpolator as the window, but need to adjust it to
+        // interpolate over the remaining progress (end - start).
+        TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
+                interpolator, start, end);
+        if (mLauncherTransitionController == null) {
+            return;
+        }
+        if (start == end || duration <= 0) {
+            mLauncherTransitionController.dispatchSetInterpolator(t -> end);
+        } else {
+            mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
+        }
+        mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
+
+        if (UNSTABLE_SPRINGS.get()) {
+            mLauncherTransitionController.dispatchOnStart();
+        }
+        mLauncherTransitionController.getAnimationPlayer().start();
+        mHasLauncherTransitionControllerStarted = true;
+    }
+
+    private void computeRecentsScrollIfInvisible() {
+        if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
+            // Views typically don't compute scroll when invisible as an optimization,
+            // but in our case we need to since the window offset depends on the scroll.
+            mRecentsView.computeScroll();
+        }
+    }
+
+    private void continueComputingRecentsScrollIfNecessary() {
+        if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
+                && !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)
+                && !mCanceled) {
+            computeRecentsScrollIfInvisible();
+            mRecentsView.postOnAnimation(this::continueComputingRecentsScrollIfNecessary);
+        }
+    }
+
+    /**
+     * Creates an animation that transforms the current app window into the home app.
+     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+     * @param homeAnimationFactory The home animation factory.
+     */
+    @Override
+    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+            HomeAnimationFactory homeAnimationFactory) {
+        RectFSpringAnim anim =
+                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
+        anim.addOnUpdateListener((r, p) -> {
+            updateSysUiFlags(Math.max(p, mCurrentShift.value));
+        });
+        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);
+                }
+                // Make sure recents is in its final state
+                maybeUpdateRecentsAttachedState(false);
+                mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
+            }
+        });
+        return anim;
+    }
+
+    @Override
+    public void onConsumerAboutToBeSwitched() {
+        if (mActivity != null) {
+            // 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();
+        }
+        if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
+            cancelCurrentAnimation();
+        } else {
+            reset();
+        }
+    }
+
+    public boolean isCanceled() {
+        return mCanceled;
+    }
+
+    @UiThread
+    private void resumeLastTask() {
+        mRecentsAnimationController.finish(false /* toRecents */, null);
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+        doLogGesture(LAST_TASK);
+        reset();
+    }
+
+    @UiThread
+    private void startNewTask() {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
+        } else {
+            startNewTaskInternal();
+        }
+    }
+
+    @UiThread
+    private void startNewTaskInternal() {
+        startNewTask(success -> {
+            if (!success) {
+                reset();
+                // We couldn't launch the task, so take user to overview so they can
+                // decide what to do instead of staying in this broken state.
+                endLauncherTransitionController();
+                updateSysUiFlags(1 /* windowProgress == overview */);
+            }
+            doLogGesture(NEW_TASK);
+        });
+    }
+
+    @Override
+    protected void onRestartPreviouslyAppearedTask() {
+        super.onRestartPreviouslyAppearedTask();
+        reset();
+    }
+
+    private void reset() {
+        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+    }
+
+    /**
+     * Cancels any running animation so that the active target can be overriden by a new swipe
+     * handle (in case of quick switch).
+     */
+    private void cancelCurrentAnimation() {
+        mCanceled = true;
+        mCurrentShift.cancelAnimation();
+        if (mLauncherTransitionController != null && mLauncherTransitionController
+                .getAnimationPlayer().isStarted()) {
+            mLauncherTransitionController.getAnimationPlayer().cancel();
+        }
+    }
+
+    private void invalidateHandler() {
+        endRunningWindowAnim(false /* cancel */);
+
+        if (mGestureEndCallback != null) {
+            mGestureEndCallback.run();
+        }
+
+        mActivityInitListener.unregister();
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
+        mTaskSnapshot = null;
+    }
+
+    private void invalidateHandlerWithLauncher() {
+        endLauncherTransitionController();
+
+        mRecentsView.onGestureAnimationEnd();
+        resetLauncherListenersAndOverlays();
+    }
+
+    private void endLauncherTransitionController() {
+        setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+        if (mLauncherTransitionController != null) {
+            mLauncherTransitionController.getAnimationPlayer().end();
+            mLauncherTransitionController = null;
+        }
+    }
+
+    private void resetLauncherListenersAndOverlays() {
+        // Reset the callback for deferred activity launches
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mActivityInterface.setOnDeferredActivityLaunchCallback(null);
+        }
+        mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+        removeLiveTileOverlay();
+    }
+
+    private void notifyTransitionCancelled() {
+        mAnimationFactory.onTransitionCancelled();
+    }
+
+    private void resetStateForAnimationCancel() {
+        boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
+        mActivityInterface.onTransitionCancelled(wasVisible);
+
+        // Leave the pending invisible flag, as it may be used by wallpaper open animation.
+        mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
+    }
+
+    protected void switchToScreenshot() {
+        final int runningTaskId = mGestureState.getRunningTaskId();
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mRecentsAnimationController != null) {
+                mRecentsAnimationController.getController().setWillFinishToHome(true);
+                // Update the screenshot of the task
+                if (mTaskSnapshot == null) {
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
+                }
+                mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, false /* refreshNow */);
+            }
+            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else if (!hasTargets()) {
+            // If there are no targets, then we don't need to capture anything
+            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else {
+            boolean finishTransitionPosted = false;
+            if (mRecentsAnimationController != null) {
+                // Update the screenshot of the task
+                if (mTaskSnapshot == null) {
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
+                }
+                final TaskView taskView;
+                if (mGestureState.getEndTarget() == HOME) {
+                    // Capture the screenshot before finishing the transition to home to ensure it's
+                    // taken in the correct orientation, but no need to update the thumbnail.
+                    taskView = null;
+                } else {
+                    taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot);
+                }
+                if (taskView != null && !mCanceled) {
+                    // Defer finishing the animation until the next launcher frame with the
+                    // new thumbnail
+                    finishTransitionPosted = ViewUtils.postDraw(taskView,
+                            () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
+                                    this::isCanceled);
+                }
+            }
+            if (!finishTransitionPosted) {
+                // If we haven't posted a draw callback, set the state immediately.
+                Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
+                        TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
+                mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+                TraceHelper.INSTANCE.endSection(traceToken);
+            }
+        }
+    }
+
+    private void finishCurrentTransitionToRecents() {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+        } 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);
+        } else {
+            mRecentsAnimationController.finish(true /* toRecents */,
+                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+        }
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
+    }
+
+    private void finishCurrentTransitionToHome() {
+        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);
+        } else {
+            finishRecentsControllerToHome(
+                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+        }
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
+        doLogGesture(HOME);
+    }
+
+    protected abstract void finishRecentsControllerToHome(Runnable callback);
+
+    private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
+        endLauncherTransitionController();
+        mActivityInterface.onSwipeUpToRecentsComplete();
+        if (mRecentsAnimationController != null) {
+            mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
+                    true /* screenshot */);
+        }
+        mRecentsView.onSwipeUpAnimationSuccess();
+
+        SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
+        doLogGesture(RECENTS);
+        reset();
+    }
+
+    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;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
deleted file mode 100644
index 8c5a788..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.fallback.FallbackRecentsView.ZOOM_PROGRESS;
-import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.RectF;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-
-/**
- * {@link ActivityControlHelper} for recents when the default launcher is different than the
- * currently running one and apps should interact with the {@link RecentsActivity} as opposed
- * to the in-launcher one.
- */
-public final class FallbackActivityControllerHelper implements
-        ActivityControlHelper<RecentsActivity> {
-
-    public FallbackActivityControllerHelper() { }
-
-    @Override
-    public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) {
-        // TODO:
-    }
-
-    @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        LayoutUtils.calculateFallbackTaskSize(context, dp, outRect);
-        if (dp.isVerticalBarLayout()
-                && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
-            Rect targetInsets = dp.getInsets();
-            int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
-            return dp.hotseatBarSizePx + hotseatInset;
-        } else {
-            return dp.heightPx - outRect.bottom;
-        }
-    }
-
-    @Override
-    public void onSwipeUpToRecentsComplete(RecentsActivity activity) {
-        RecentsView recentsView = activity.getOverviewPanel();
-        recentsView.getClearAllButton().setVisibilityAlpha(1);
-        recentsView.setDisallowScrollToClearAll(false);
-    }
-
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        // This class becomes active when the screen is locked.
-        // Rather than having it handle assistant visibility changes, the assistant visibility is
-        // set to zero prior to this class becoming active.
-    }
-
-    @NonNull
-    @Override
-    public HomeAnimationFactory prepareHomeUI(RecentsActivity activity) {
-        RecentsView recentsView = activity.getOverviewPanel();
-
-        return new HomeAnimationFactory() {
-            @NonNull
-            @Override
-            public RectF getWindowTargetRect() {
-                float centerX = recentsView.getPivotX();
-                float centerY = recentsView.getPivotY();
-                return new RectF(centerX, centerY, centerX, centerY);
-            }
-
-            @NonNull
-            @Override
-            public AnimatorPlaybackController createActivityAnimationToHome() {
-                Animator anim = ObjectAnimator.ofFloat(recentsView, CONTENT_ALPHA, 0);
-                anim.addListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        recentsView.startHome();
-                    }
-                });
-                AnimatorSet animatorSet = new AnimatorSet();
-                animatorSet.play(anim);
-                long accuracy = 2 * Math.max(recentsView.getWidth(), recentsView.getHeight());
-                return AnimatorPlaybackController.wrap(animatorSet, accuracy);
-            }
-        };
-    }
-
-    @Override
-    public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        if (activityVisible) {
-            return (transitionLength) -> { };
-        }
-
-        FallbackRecentsView rv = activity.getOverviewPanel();
-        rv.setContentAlpha(0);
-        rv.getClearAllButton().setVisibilityAlpha(0);
-        rv.setDisallowScrollToClearAll(true);
-
-        boolean fromState = !animateActivity;
-        rv.setInOverviewState(fromState);
-
-        return new AnimationFactory() {
-
-            boolean isAnimatingToRecents = false;
-
-            @Override
-            public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) {
-                isAnimatingToRecents = targets != null && targets.isAnimatingHome();
-                if (!isAnimatingToRecents) {
-                    rv.setContentAlpha(1);
-                }
-                createActivityController(getSwipeUpDestinationAndLength(
-                        activity.getDeviceProfile(), activity, new Rect()));
-            }
-
-            @Override
-            public void createActivityController(long transitionLength) {
-                AnimatorSet animatorSet = new AnimatorSet();
-                if (isAnimatingToRecents) {
-                    ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1);
-                    anim.setDuration(transitionLength).setInterpolator(LINEAR);
-                    animatorSet.play(anim);
-                }
-
-                ObjectAnimator anim = ObjectAnimator.ofFloat(rv, ZOOM_PROGRESS, 1, 0);
-                anim.setDuration(transitionLength).setInterpolator(LINEAR);
-                animatorSet.play(anim);
-
-                AnimatorPlaybackController controller =
-                        AnimatorPlaybackController.wrap(animatorSet, transitionLength);
-
-                // Since we are changing the start position of the UI, reapply the state, at the end
-                controller.setEndAction(() -> {
-                    boolean endState = true;
-                    rv.setInOverviewState(controller.getInterpolatedProgress() > 0.5 ?
-                                    endState : fromState);
-                });
-
-                callback.accept(controller);
-            }
-        };
-    }
-
-    @Override
-    public ActivityInitListener createActivityInitListener(
-            BiPredicate<RecentsActivity, Boolean> onInitListener) {
-        return new RecentsActivityTracker(onInitListener);
-    }
-
-    @Nullable
-    @Override
-    public RecentsActivity getCreatedActivity() {
-        return RecentsActivityTracker.getCurrentActivity();
-    }
-
-    @Nullable
-    @Override
-    public RecentsView getVisibleRecentsView() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity != null && activity.hasWindowFocus()) {
-            return activity.getOverviewPanel();
-        }
-        return null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        return false;
-    }
-
-    @Override
-    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
-        // TODO: Remove this once b/77875376 is fixed
-        return target.sourceContainerBounds;
-    }
-
-    @Override
-    public boolean shouldMinimizeSplitScreen() {
-        // TODO: Remove this once b/77875376 is fixed
-        return false;
-    }
-
-    @Override
-    public int getContainerType() {
-        RecentsActivity activity = getCreatedActivity();
-        boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
-        return visible
-                ? LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER
-                : LauncherLogProto.ContainerType.APP;
-    }
-
-    @Override
-    public boolean isInLiveTileMode() {
-        return false;
-    }
-
-    @Override
-    public void onLaunchTaskFailed(RecentsActivity activity) {
-        // TODO: probably go back to overview instead.
-        activity.<RecentsView>getOverviewPanel().startHome();
-    }
-
-    @Override
-    public void onLaunchTaskSuccess(RecentsActivity activity) {
-        activity.onTaskLaunched();
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
new file mode 100644
index 0000000..33b9cde
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseActivityInterface} for recents when the default launcher is different than the
+ * currently running one and apps should interact with the {@link RecentsActivity} as opposed
+ * to the in-launcher one.
+ */
+public final class FallbackActivityInterface extends
+        BaseActivityInterface<RecentsState, RecentsActivity> {
+
+    public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
+
+    private FallbackActivityInterface() {
+        super(false, DEFAULT, BACKGROUND_APP);
+    }
+
+    /** 2 */
+    @Override
+    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+            PagedOrientationHandler orientationHandler) {
+        calculateTaskSize(context, dp, outRect, orientationHandler);
+        if (dp.isVerticalBarLayout()
+                && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
+            Rect targetInsets = dp.getInsets();
+            int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+            return dp.hotseatBarSizePx + hotseatInset;
+        } else {
+            return dp.heightPx - outRect.bottom;
+        }
+    }
+
+    /** 4 */
+    @Override
+    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
+        onSwipeUpToRecentsComplete();
+    }
+
+    /** 5 */
+    @Override
+    public void onAssistantVisibilityChanged(float visibility) {
+        // This class becomes active when the screen is locked.
+        // Rather than having it handle assistant visibility changes, the assistant visibility is
+        // set to zero prior to this class becoming active.
+    }
+
+    /** 6 */
+    @Override
+    public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+        DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
+        factory.initUI();
+        return factory;
+    }
+
+    @Override
+    public ActivityInitListener createActivityInitListener(
+            Predicate<Boolean> onInitListener) {
+        return new ActivityInitListener<>((activity, alreadyOnHome) ->
+                onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
+    }
+
+    @Nullable
+    @Override
+    public RecentsActivity getCreatedActivity() {
+        return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+    }
+
+    @Nullable
+    @Override
+    public RecentsView getVisibleRecentsView() {
+        RecentsActivity activity = getCreatedActivity();
+        if (activity != null && activity.hasWindowFocus()) {
+            return activity.getOverviewPanel();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+        return false;
+    }
+
+    @Override
+    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+        // TODO: Remove this once b/77875376 is fixed
+        return target.screenSpaceBounds;
+    }
+
+    @Override
+    public boolean allowMinimizeSplitScreen() {
+        // TODO: Remove this once b/77875376 is fixed
+        return false;
+    }
+
+    @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        // In non-gesture mode, user might be clicking on the home button which would directly
+        // start the home activity instead of going through recents. In that case, defer starting
+        // recents until we are sure it is a gesture.
+        return !deviceState.isFullyGesturalNavMode()
+                || super.deferStartingActivity(deviceState, ev);
+    }
+
+    @Override
+    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+        // no-op, fake landscape not supported for 3P
+    }
+
+    @Override
+    public int getContainerType() {
+        RecentsActivity activity = getCreatedActivity();
+        boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
+        return visible
+                ? LauncherLogProto.ContainerType.OTHER_LAUNCHER_APP
+                : LauncherLogProto.ContainerType.APP;
+    }
+
+    @Override
+    public boolean isInLiveTileMode() {
+        return false;
+    }
+
+    @Override
+    public void onLaunchTaskFailed() {
+        // TODO: probably go back to overview instead.
+        RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.<RecentsView>getOverviewPanel().startHome();
+    }
+
+    @Override
+    protected float getExtraSpace(Context context, DeviceProfile dp,
+            PagedOrientationHandler orientationHandler) {
+        return showOverviewActions(context)
+                ? context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height)
+                : 0;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
new file mode 100644
index 0000000..fc7a119
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+
+import android.animation.ObjectAnimator;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+/**
+ * Handles the navigation gestures when a 3rd party launcher is the default home activity.
+ */
+public class FallbackSwipeHandler extends
+        BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
+
+    private FallbackHomeAnimationFactory mActiveAnimationFactory;
+    private final boolean mRunningOverHome;
+
+    private final Matrix mTmpMatrix = new Matrix();
+    private float mMaxLauncherScale = 1;
+
+    public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer);
+
+        mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
+        if (mRunningOverHome) {
+            mTransformParams.setHomeBuilderProxy(this::updateHomeActivityTransformDuringSwipeUp);
+        }
+    }
+
+    @Override
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        if (mRunningOverHome) {
+            mMaxLauncherScale = 1 / mTaskViewSimulator.getFullScreenScale();
+        }
+    }
+
+    private void updateHomeActivityTransformDuringSwipeUp(SurfaceParams.Builder builder,
+            RemoteAnimationTargetCompat app, TransformParams params) {
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+                Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
+    }
+
+    private void setHomeScaleAndAlpha(SurfaceParams.Builder builder,
+            RemoteAnimationTargetCompat app, float verticalShift, float alpha) {
+        float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale);
+        mTmpMatrix.setScale(scale, scale,
+                app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+        builder.withMatrix(mTmpMatrix).withAlpha(alpha);
+    }
+
+    @Override
+    protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
+        mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
+        ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+        mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+        return mActiveAnimationFactory;
+    }
+
+    @Override
+    protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+        if (mActiveAnimationFactory != null
+                && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
+            mActiveAnimationFactory = null;
+            return false;
+        }
+
+        return super.handleTaskAppeared(appearedTaskTarget);
+    }
+
+    @Override
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        mRecentsAnimationController.finish(
+                false /* toRecents */, callback, true /* sendUserLeaveHint */);
+    }
+
+    @Override
+    protected void switchToScreenshot() {
+        if (mRunningOverHome) {
+            // When the current task is home, then we don't need to capture anything
+            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else {
+            super.switchToScreenshot();
+        }
+    }
+
+    @Override
+    protected void notifyGestureAnimationStartToRecents() {
+        if (mRunningOverHome) {
+            mRecentsView.onGestureAnimationStartOnHome(mGestureState.getRunningTask());
+        } else {
+            super.notifyGestureAnimationStartToRecents();
+        }
+    }
+
+    private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
+
+        private final TransformParams mHomeAlphaParams = new TransformParams();
+        private final AnimatedFloat mHomeAlpha;
+
+        private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
+
+        private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
+
+        private final long mDuration;
+        FallbackHomeAnimationFactory(long duration) {
+            super(null);
+            mDuration = duration;
+
+            if (mRunningOverHome) {
+                mHomeAlpha = new AnimatedFloat();
+                mHomeAlpha.value = Utilities.boundToRange(1 - mCurrentShift.value, 0, 1);
+                mVerticalShiftForScale.value = mCurrentShift.value;
+                mTransformParams.setHomeBuilderProxy(
+                        this::updateHomeActivityTransformDuringHomeAnim);
+            } else {
+                mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
+                mHomeAlpha.value = 0;
+
+                mHomeAlphaParams.setHomeBuilderProxy(
+                        this::updateHomeActivityTransformDuringHomeAnim);
+            }
+
+            mRecentsAlpha.value = 1;
+            mTransformParams.setBaseBuilderProxy(
+                    this::updateRecentsActivityTransformDuringHomeAnim);
+        }
+
+        private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
+                RemoteAnimationTargetCompat app, TransformParams params) {
+            builder.withAlpha(mRecentsAlpha.value);
+        }
+
+        private void updateHomeActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
+                RemoteAnimationTargetCompat app, TransformParams params) {
+            setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
+        }
+
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            PendingAnimation pa = new PendingAnimation(mDuration);
+            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCEL);
+            return pa.createPlaybackController();
+        }
+
+        private void updateHomeAlpha() {
+            if (mHomeAlphaParams.getTargetSet() != null) {
+                mHomeAlphaParams.applySurfaceParams(
+                        mHomeAlphaParams.createSurfaceParams(BuilderProxy.NO_OP));
+            }
+        }
+
+        public boolean handleHomeTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+            if (appearedTaskTarget.activityType == ACTIVITY_TYPE_HOME) {
+                RemoteAnimationTargets targets = new RemoteAnimationTargets(
+                        new RemoteAnimationTargetCompat[] {appearedTaskTarget},
+                        new RemoteAnimationTargetCompat[0], appearedTaskTarget.mode);
+                mHomeAlphaParams.setTargetSet(targets);
+                updateHomeAlpha();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            ObjectAnimator alphaAnim = mHomeAlpha.animateToValue(mHomeAlpha.value, 1);
+            alphaAnim.setDuration(mDuration).setInterpolator(ACCEL);
+            alphaAnim.start();
+
+            if (mRunningOverHome) {
+                // Spring back launcher scale
+                new SpringAnimationBuilder(mContext)
+                        .setStartValue(mVerticalShiftForScale.value)
+                        .setEndValue(0)
+                        .setStartVelocity(-velocity / mTransitionDragLength)
+                        .setMinimumVisibleChange(1f / mDp.heightPx)
+                        .setDampingRatio(0.6f)
+                        .setStiffness(800)
+                        .build(mVerticalShiftForScale, AnimatedFloat.VALUE)
+                        .start();
+            }
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
new file mode 100644
index 0000000..ba8ba33
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static android.content.Intent.EXTRA_STREAM;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.ImageActionUtils.persistBitmapAndStartActivity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BuildConfig;
+import com.android.quickstep.util.ImageActionUtils;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.function.Supplier;
+
+/**
+ * Contains image selection functions necessary to complete overview action button functions.
+ */
+public class ImageActionsApi {
+
+    private static final String TAG = BuildConfig.APPLICATION_ID + "ImageActionsApi";
+
+    protected final Context mContext;
+    protected final Supplier<Bitmap> mBitmapSupplier;
+    protected final SystemUiProxy mSystemUiProxy;
+
+    public ImageActionsApi(Context context, Supplier<Bitmap> bitmapSupplier) {
+        mContext = context;
+        mBitmapSupplier = bitmapSupplier;
+        mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
+    }
+
+    /**
+     * Share the image this api was constructed with using the provided intent. The implementation
+     * should add an {@link Intent#EXTRA_STREAM} with the URI pointing to the image to the intent.
+     */
+    @UiThread
+    public void shareWithExplicitIntent(@Nullable Rect crop, Intent intent) {
+        if (mBitmapSupplier.get() == null) {
+            Log.e(TAG, "No snapshot available, not starting share.");
+            return;
+        }
+
+        UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
+                mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
+                    intentForUri
+                            .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+                            .putExtra(EXTRA_STREAM, uri);
+                    return new Intent[]{intentForUri};
+                }, TAG));
+
+    }
+
+    /**
+     * Share the image this api was constructed with.
+     */
+    @UiThread
+    public void startShareActivity() {
+        ImageActionUtils.startShareActivity(mContext, mBitmapSupplier, null, null, TAG);
+    }
+
+    /**
+     * @param screenshot       to be saved to the media store.
+     * @param screenshotBounds the location of where the bitmap was laid out on the screen in
+     *                         screen coordinates.
+     * @param visibleInsets    that are used to draw the screenshot within the bounds.
+     * @param task             of the task that the screenshot was taken of.
+     */
+    public void saveScreenshot(Bitmap screenshot, Rect screenshotBounds,
+            Insets visibleInsets, Task.TaskKey task) {
+        ImageActionUtils.saveScreenshot(mSystemUiProxy, screenshot, screenshotBounds, visibleInsets,
+                task);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
deleted file mode 100644
index 54a366d..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.view.View.TRANSLATION_Y;
-
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_FADE_ANIM;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_TRANSLATE_X_ANIM;
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
-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.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.os.UserHandle;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherInitListenerEx;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
-import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
-import com.android.quickstep.views.LauncherRecentsView;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-
-/**
- * {@link ActivityControlHelper} for the in-launcher recents.
- */
-public final class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
-
-    private Runnable mAdjustInterpolatorsRunnable;
-
-    @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        LayoutUtils.calculateLauncherTaskSize(context, dp, outRect);
-        if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
-            Rect targetInsets = dp.getInsets();
-            int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
-            return dp.hotseatBarSizePx + hotseatInset;
-        } else {
-            return LayoutUtils.getShelfTrackingDistance(context, dp);
-        }
-    }
-
-    @Override
-    public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
-        LauncherState startState = activity.getStateManager().getRestState();
-        activity.getStateManager().goToState(startState, activityVisible);
-    }
-
-    @Override
-    public void onSwipeUpToRecentsComplete(Launcher activity) {
-        // Re apply state in case we did something funky during the transition.
-        activity.getStateManager().reapplyState();
-        DiscoveryBounce.showForOverviewIfNeeded(activity);
-    }
-
-    @Override
-    public void onSwipeUpToHomeComplete(Launcher activity) {
-        // Ensure recents is at the correct position for NORMAL state. For example, when we detach
-        // recents, we assume the first task is invisible, making translation off by one task.
-        activity.getStateManager().reapplyState();
-    }
-
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.onAssistantVisibilityChanged(visibility);
-        }
-    }
-
-    @NonNull
-    @Override
-    public HomeAnimationFactory prepareHomeUI(Launcher activity) {
-        final DeviceProfile dp = activity.getDeviceProfile();
-        final RecentsView recentsView = activity.getOverviewPanel();
-        final TaskView runningTaskView = recentsView.getRunningTaskView();
-        final View workspaceView;
-        if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
-            workspaceView = activity.getWorkspace().getFirstMatchForAppClose(
-                    runningTaskView.getTask().key.getComponent().getPackageName(),
-                    UserHandle.of(runningTaskView.getTask().key.userId));
-        } else {
-            workspaceView = null;
-        }
-        final RectF iconLocation = new RectF();
-        boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
-        FloatingIconView floatingIconView = canUseWorkspaceView
-                ? FloatingIconView.getFloatingIconView(activity, workspaceView,
-                        true /* hideOriginal */, iconLocation, false /* isOpening */)
-                : null;
-
-        return new HomeAnimationFactory() {
-            @Nullable
-            @Override
-            public View getFloatingView() {
-                return floatingIconView;
-            }
-
-            @NonNull
-            @Override
-            public RectF getWindowTargetRect() {
-                if (canUseWorkspaceView) {
-                    return iconLocation;
-                } else {
-                    return HomeAnimationFactory.getDefaultWindowTargetRect(dp);
-                }
-            }
-
-            @NonNull
-            @Override
-            public AnimatorPlaybackController createActivityAnimationToHome() {
-                // Return an empty APC here since we have an non-user controlled animation to home.
-                long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
-                return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
-                        0 /* animComponents */);
-            }
-
-            @Override
-            public void playAtomicAnimation(float velocity) {
-                new StaggeredWorkspaceAnim(activity, velocity, true /* animateOverviewScrim */)
-                        .start();
-            }
-        };
-    }
-
-    @Override
-    public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        final LauncherState startState = activity.getStateManager().getState();
-
-        LauncherState resetState = startState;
-        if (startState.disableRestore) {
-            resetState = activity.getStateManager().getRestState();
-        }
-        activity.getStateManager().setRestState(resetState);
-
-        final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
-        activity.getStateManager().goToState(fromState, false);
-        // Since all apps is not visible, we can safely reset the scroll position.
-        // This ensures then the next swipe up to all-apps starts from scroll 0.
-        activity.getAppsView().reset(false /* animate */);
-
-        return new AnimationFactory() {
-            private final ShelfPeekAnim mShelfAnim =
-                    ((QuickstepAppTransitionManagerImpl) activity.getAppTransitionManager())
-                            .getShelfPeekAnim();
-            private boolean mIsAttachedToWindow;
-
-            @Override
-            public void createActivityController(long transitionLength) {
-                createActivityControllerInternal(activity, fromState, transitionLength, callback);
-                // Creating the activity controller animation sometimes reapplies the launcher state
-                // (because we set the animation as the current state animation), so we reapply the
-                // attached state here as well to ensure recents is shown/hidden appropriately.
-                if (SysUINavigationMode.getMode(activity) == Mode.NO_BUTTON) {
-                    setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
-                }
-            }
-
-            @Override
-            public void adjustActivityControllerInterpolators() {
-                if (mAdjustInterpolatorsRunnable != null) {
-                    mAdjustInterpolatorsRunnable.run();
-                }
-            }
-
-            @Override
-            public void onTransitionCancelled() {
-                activity.getStateManager().goToState(startState, false /* animate */);
-            }
-
-            @Override
-            public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
-                    long duration) {
-                mShelfAnim.setShelfState(shelfState, interpolator, duration);
-            }
-
-            @Override
-            public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
-                if (mIsAttachedToWindow == attached && animate) {
-                    return;
-                }
-                mIsAttachedToWindow = attached;
-                LauncherRecentsView recentsView = activity.getOverviewPanel();
-                Animator fadeAnim = activity.getStateManager()
-                        .createStateElementAnimation(
-                        INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
-
-                int runningTaskIndex = recentsView.getRunningTaskIndex();
-                if (runningTaskIndex == recentsView.getTaskViewStartIndex()) {
-                    // If we are on the first task (we haven't quick switched), translate recents in
-                    // from the side. Calculate the start translation based on current scale/scroll.
-                    float currScale = recentsView.getScaleX();
-                    float scrollOffsetX = recentsView.getScrollOffset();
-                    float offscreenX = recentsView.getOffscreenTranslationX(currScale);
-
-                    float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
-                    float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
-                    activity.getStateManager()
-                            .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-
-                    if (!recentsView.isShown() && animate) {
-                        recentsView.setTranslationX(fromTranslationX);
-                    } else {
-                        fromTranslationX = recentsView.getTranslationX();
-                    }
-
-                    if (!animate) {
-                        recentsView.setTranslationX(toTranslationX);
-                    } else {
-                        activity.getStateManager().createStateElementAnimation(
-                                INDEX_RECENTS_TRANSLATE_X_ANIM,
-                                fromTranslationX, toTranslationX).start();
-                    }
-
-                    fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
-                } else {
-                    fadeAnim.setInterpolator(ACCEL_DEACCEL);
-                }
-                fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
-            }
-        };
-    }
-
-    private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
-            long transitionLength, Consumer<AnimatorPlaybackController> callback) {
-        LauncherState endState = OVERVIEW;
-        if (fromState == endState) {
-            return;
-        }
-
-        AnimatorSet anim = new AnimatorSet();
-        if (!activity.getDeviceProfile().isVerticalBarLayout()
-                && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
-            // Don't animate the shelf when the mode is NO_BUTTON, because we update it atomically.
-            anim.play(activity.getStateManager().createStateElementAnimation(
-                    INDEX_SHELF_ANIM,
-                    fromState.getVerticalProgress(activity),
-                    endState.getVerticalProgress(activity)));
-        }
-        playScaleDownAnim(anim, activity, fromState, endState);
-
-        anim.setDuration(transitionLength * 2);
-        anim.setInterpolator(LINEAR);
-        AnimatorPlaybackController controller =
-                AnimatorPlaybackController.wrap(anim, transitionLength * 2);
-        activity.getStateManager().setCurrentUserControlledAnimation(controller);
-
-        // Since we are changing the start position of the UI, reapply the state, at the end
-        controller.setEndAction(() -> {
-            activity.getStateManager().goToState(
-                    controller.getInterpolatedProgress() > 0.5 ? endState : fromState, false);
-        });
-        callback.accept(controller);
-    }
-
-    /**
-     * Scale down recents from the center task being full screen to being in overview.
-     */
-    private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, LauncherState fromState,
-            LauncherState endState) {
-        RecentsView recentsView = launcher.getOverviewPanel();
-        if (recentsView.getCurrentPageTaskView() == null) {
-            return;
-        }
-
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation
-                = fromState.getOverviewScaleAndTranslation(launcher);
-        LauncherState.ScaleAndTranslation endScaleAndTranslation
-                = endState.getOverviewScaleAndTranslation(launcher);
-        float fromTranslationY = fromScaleAndTranslation.translationY;
-        float endTranslationY = endScaleAndTranslation.translationY;
-        float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
-        float endFullscreenProgress = endState.getOverviewFullscreenProgress();
-
-        Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
-                fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
-        Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
-                fromTranslationY, endTranslationY);
-        Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
-                RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
-        anim.playTogether(scale, translateY, applyFullscreenProgress);
-
-        mAdjustInterpolatorsRunnable = () -> {
-            // Adjust the translateY interpolator to account for the running task's top inset.
-            // When progress <= 1, this is handled by each task view as they set their fullscreen
-            // progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
-            // recents as a whole needs to translate further to keep up with the app window.
-            TaskView runningTaskView = recentsView.getRunningTaskView();
-            if (runningTaskView == null) {
-                runningTaskView = recentsView.getCurrentPageTaskView();
-                if (runningTaskView == null) {
-                    // There are no task views in LockTask mode when Overview is enabled.
-                    return;
-                }
-            }
-            TimeInterpolator oldInterpolator = translateY.getInterpolator();
-            Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
-            float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
-            float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
-            translateY.setInterpolator(t -> {
-                float newT = oldInterpolator.getInterpolation(t);
-                return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
-            });
-        };
-    }
-
-    @Override
-    public ActivityInitListener createActivityInitListener(
-            BiPredicate<Launcher, Boolean> onInitListener) {
-        return new LauncherInitListenerEx(onInitListener);
-    }
-
-    @Nullable
-    @Override
-    public Launcher getCreatedActivity() {
-        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-        if (app == null) {
-            return null;
-        }
-        return (Launcher) app.getModel().getCallback();
-    }
-
-    @Nullable
-    @UiThread
-    private Launcher getVisibleLauncher() {
-        Launcher launcher = getCreatedActivity();
-        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
-                launcher : null;
-    }
-
-    @Nullable
-    @Override
-    public RecentsView getVisibleRecentsView() {
-        Launcher launcher = getVisibleLauncher();
-        return launcher != null && launcher.getStateManager().getState().overviewUi
-                ? launcher.getOverviewPanel() : null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        Launcher launcher = getVisibleLauncher();
-        if (launcher == null) {
-            return false;
-        }
-
-        launcher.getUserEventDispatcher().logActionCommand(
-                LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                getContainerType(),
-                LauncherLogProto.ContainerType.TASKSWITCHER);
-        launcher.getStateManager().goToState(OVERVIEW,
-                launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
-        return true;
-    }
-
-    @Override
-    public boolean deferStartingActivity(Region activeNavBarRegion, MotionEvent ev) {
-        return activeNavBarRegion.contains((int) ev.getX(), (int) ev.getY());
-    }
-
-    @Override
-    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
-        return homeBounds;
-    }
-
-    @Override
-    public boolean shouldMinimizeSplitScreen() {
-        return true;
-    }
-
-    @Override
-    public int getContainerType() {
-        final Launcher launcher = getVisibleLauncher();
-        return launcher != null ? launcher.getStateManager().getState().containerType
-                : LauncherLogProto.ContainerType.APP;
-    }
-
-    @Override
-    public boolean isInLiveTileMode() {
-        Launcher launcher = getCreatedActivity();
-        return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
-                launcher.isStarted();
-    }
-
-    @Override
-    public void onLaunchTaskFailed(Launcher launcher) {
-        launcher.getStateManager().goToState(OVERVIEW);
-    }
-
-    @Override
-    public void onLaunchTaskSuccess(Launcher launcher) {
-        launcher.getStateManager().moveToRestState();
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
new file mode 100644
index 0000000..edefbe1
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
+import static com.android.quickstep.SysUINavigationMode.getMode;
+import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
+import static com.android.quickstep.util.LayoutUtils.getDefaultSwipeHeight;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.shared.LauncherOverlayManager;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseActivityInterface} for the in-launcher recents.
+ */
+public final class LauncherActivityInterface extends
+        BaseActivityInterface<LauncherState, BaseQuickstepLauncher> {
+
+    public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
+
+    private LauncherActivityInterface() {
+        super(true, OVERVIEW, BACKGROUND_APP);
+    }
+
+    @Override
+    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+            PagedOrientationHandler orientationHandler) {
+        calculateTaskSize(context, dp, outRect, orientationHandler);
+        if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
+            Rect targetInsets = dp.getInsets();
+            int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+            return dp.hotseatBarSizePx + hotseatInset;
+        } else {
+            return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler);
+        }
+    }
+
+    @Override
+    public void onSwipeUpToRecentsComplete() {
+        super.onSwipeUpToRecentsComplete();
+        Launcher launcher = getCreatedActivity();
+        if (launcher != null) {
+            RecentsView recentsView = launcher.getOverviewPanel();
+            DiscoveryBounce.showForOverviewIfNeeded(launcher,
+                    recentsView.getPagedOrientationHandler());
+        }
+    }
+
+    @Override
+    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        // Ensure recents is at the correct position for NORMAL state. For example, when we detach
+        // recents, we assume the first task is invisible, making translation off by one task.
+        launcher.getStateManager().reapplyState();
+        launcher.getRootView().setForceHideBackArrow(false);
+        notifyRecentsOfOrientation(deviceState);
+    }
+
+    @Override
+    public void onAssistantVisibilityChanged(float visibility) {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        launcher.onAssistantVisibilityChanged(visibility);
+    }
+
+    @Override
+    public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+        notifyRecentsOfOrientation(deviceState);
+        DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
+            @Override
+            public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
+                    long duration) {
+                mActivity.getShelfPeekAnim().setShelfState(shelfState, interpolator, duration);
+            }
+
+            @Override
+            protected void createBackgroundToOverviewAnim(BaseQuickstepLauncher activity,
+                    PendingAnimation pa) {
+                super.createBackgroundToOverviewAnim(activity, pa);
+
+                if (!activity.getDeviceProfile().isVerticalBarLayout()
+                        && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
+                    // Don't animate the shelf when the mode is NO_BUTTON, because we
+                    // update it atomically.
+                    pa.add(activity.getStateManager().createStateElementAnimation(
+                            INDEX_SHELF_ANIM,
+                            BACKGROUND_APP.getVerticalProgress(activity),
+                            OVERVIEW.getVerticalProgress(activity)));
+                }
+
+                // Animate the blur and wallpaper zoom
+                float fromDepthRatio = BACKGROUND_APP.getDepth(activity);
+                float toDepthRatio = OVERVIEW.getDepth(activity);
+                pa.addFloat(getDepthController(),
+                        new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
+                        fromDepthRatio, toDepthRatio, LINEAR);
+
+            }
+        };
+
+        BaseQuickstepLauncher launcher = factory.initUI();
+        // Since all apps is not visible, we can safely reset the scroll position.
+        // This ensures then the next swipe up to all-apps starts from scroll 0.
+        launcher.getAppsView().reset(false /* animate */);
+        return factory;
+    }
+
+    @Override
+    public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
+        return new LauncherInitListener((activity, alreadyOnHome) ->
+                onInitListener.test(alreadyOnHome));
+    }
+
+    @Override
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        launcher.setOnDeferredActivityLaunchCallback(r);
+    }
+
+    @Nullable
+    @Override
+    public BaseQuickstepLauncher getCreatedActivity() {
+        return BaseQuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+    }
+
+    @Nullable
+    @Override
+    public DepthController getDepthController() {
+        BaseQuickstepLauncher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return null;
+        }
+        return launcher.getDepthController();
+    }
+
+    @Nullable
+    @Override
+    public RecentsView getVisibleRecentsView() {
+        Launcher launcher = getVisibleLauncher();
+        return launcher != null && launcher.getStateManager().getState().overviewUi
+                ? launcher.getOverviewPanel() : null;
+    }
+
+    @Nullable
+    @UiThread
+    private Launcher getVisibleLauncher() {
+        Launcher launcher = getCreatedActivity();
+        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus()
+                ? launcher : null;
+    }
+
+    @Override
+    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "switchToRecentsIfVisible");
+        }
+        Launcher launcher = getVisibleLauncher();
+        if (launcher == null) {
+            return false;
+        }
+
+        launcher.getUserEventDispatcher().logActionCommand(
+                LauncherLogProto.Action.Command.RECENTS_BUTTON,
+                getContainerType(),
+                LauncherLogProto.ContainerType.TASKSWITCHER);
+        launcher.getStateManager().goToState(OVERVIEW,
+                launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
+        return true;
+    }
+
+
+    @Override
+    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+        final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
+        stateManager.addStateListener(
+                new StateManager.StateListener<LauncherState>() {
+                    @Override
+                    public void onStateTransitionComplete(LauncherState toState) {
+                        // Are we going from Recents to Workspace?
+                        if (toState == LauncherState.NORMAL) {
+                            exitRunnable.run();
+                            notifyRecentsOfOrientation(deviceState);
+                            stateManager.removeStateListener(this);
+                        }
+                    }
+                });
+    }
+
+    private void notifyRecentsOfOrientation(RecentsAnimationDeviceState deviceState) {
+        // reset layout on swipe to home
+        RecentsView recentsView = getCreatedActivity().getOverviewPanel();
+        recentsView.setLayoutRotation(deviceState.getCurrentActiveRotation(),
+                deviceState.getDisplayRotation());
+    }
+
+    @Override
+    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+        return homeBounds;
+    }
+
+    @Override
+    public boolean allowMinimizeSplitScreen() {
+        return true;
+    }
+
+    @Override
+    public void updateOverviewPredictionState() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
+                PredictionUiStateManager.Client.OVERVIEW);
+    }
+
+    @Override
+    public int getContainerType() {
+        final Launcher launcher = getVisibleLauncher();
+        return launcher != null ? launcher.getStateManager().getState().containerType
+                : LauncherLogProto.ContainerType.APP;
+    }
+
+    @Override
+    public boolean isInLiveTileMode() {
+        Launcher launcher = getCreatedActivity();
+        return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
+                launcher.isStarted();
+    }
+
+    @Override
+    public void onLaunchTaskFailed() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        launcher.getStateManager().goToState(OVERVIEW);
+    }
+
+    @Override
+    public void closeOverlay() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        LauncherOverlayManager om = launcher.getOverlayManager();
+        if (!launcher.isStarted() || launcher.isForceInvisible()) {
+            om.hideOverlay(false /* animate */);
+        } else {
+            om.hideOverlay(150);
+        }
+    }
+
+    @Override
+    protected float getExtraSpace(Context context, DeviceProfile dp,
+            PagedOrientationHandler orientationHandler) {
+        if ((dp.isVerticalBarLayout() && !showOverviewActions(context))
+                || hideShelfInTwoButtonLandscape(context, orientationHandler)) {
+            return 0;
+        } else {
+            Resources res = context.getResources();
+            if (showOverviewActions(context)) {
+                //TODO: this needs to account for the swipe gesture height and accessibility
+                // UI when shown.
+                float actionsBottomMargin = 0;
+                if (!dp.isVerticalBarLayout()) {
+                    if (getMode(context) == Mode.THREE_BUTTONS) {
+                        actionsBottomMargin = res.getDimensionPixelSize(
+                            R.dimen.overview_actions_bottom_margin_three_button);
+                    } else {
+                        actionsBottomMargin = res.getDimensionPixelSize(
+                            R.dimen.overview_actions_bottom_margin_gesture);
+                    }
+                }
+                float actionsHeight = actionsBottomMargin
+                        + res.getDimensionPixelSize(R.dimen.overview_actions_height);
+                return actionsHeight;
+            } else {
+                return getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
+                        + res.getDimensionPixelSize(
+                        R.dimen.dynamic_grid_hotseat_extra_vertical_size)
+                        + res.getDimensionPixelSize(
+                        R.dimen.dynamic_grid_hotseat_bottom_padding);
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
new file mode 100644
index 0000000..fa7d268
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.UserHandle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.InputConsumerController;
+
+/**
+ * Temporary class to allow easier refactoring
+ */
+public class LauncherSwipeHandlerV2 extends
+        BaseSwipeUpHandlerV2<BaseQuickstepLauncher, RecentsView> {
+
+    public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer);
+    }
+
+
+    @Override
+    protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
+        HomeAnimationFactory homeAnimFactory;
+        if (mActivity != null) {
+            final TaskView runningTaskView = mRecentsView.getRunningTaskView();
+            final View workspaceView;
+            if (runningTaskView != null
+                    && runningTaskView.getTask().key.getComponent() != null) {
+                workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose(
+                        runningTaskView.getTask().key.getComponent().getPackageName(),
+                        UserHandle.of(runningTaskView.getTask().key.userId));
+            } else {
+                workspaceView = null;
+            }
+            final RectF iconLocation = new RectF();
+            boolean canUseWorkspaceView =
+                    workspaceView != null && workspaceView.isAttachedToWindow();
+            FloatingIconView floatingIconView = canUseWorkspaceView
+                    ? FloatingIconView.getFloatingIconView(mActivity, workspaceView,
+                    true /* hideOriginal */, iconLocation, false /* isOpening */)
+                    : null;
+
+            mActivity.getRootView().setForceHideBackArrow(true);
+            mActivity.setHintUserWillBeActive();
+
+            homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
+
+                @Override
+                public RectF getWindowTargetRect() {
+                    if (canUseWorkspaceView) {
+                        return iconLocation;
+                    } else {
+                        return super.getWindowTargetRect();
+                    }
+                }
+
+                @NonNull
+                @Override
+                public AnimatorPlaybackController createActivityAnimationToHome() {
+                    // Return an empty APC here since we have an non-user controlled animation
+                    // to home.
+                    long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+                    return mActivity.getStateManager().createAnimationToNewWorkspace(
+                            NORMAL, accuracy, 0 /* animComponents */);
+                }
+
+                @Override
+                public void playAtomicAnimation(float velocity) {
+                    new StaggeredWorkspaceAnim(mActivity, velocity,
+                            true /* animateOverviewScrim */).start();
+                }
+            };
+
+        } else {
+            homeAnimFactory = new HomeAnimationFactory(null) {
+                @Override
+                public AnimatorPlaybackController createActivityAnimationToHome() {
+                    return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
+                }
+            };
+            mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+                    isPresent -> mRecentsView.startHome());
+        }
+        return homeAnimFactory;
+    }
+
+    @Override
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        mRecentsAnimationController.finish(
+                true /* toRecents */, callback, true /* sendUserLeaveHint */);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
deleted file mode 100644
index 65f323c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-/**
- * Empty activity to start a recents transition
- */
-public class LockScreenRecentsActivity extends Activity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        finish();
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
deleted file mode 100644
index 357c9fc..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
+++ /dev/null
@@ -1,137 +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.quickstep;
-
-import android.util.Log;
-import android.util.SparseArray;
-
-import com.android.launcher3.config.FeatureFlags;
-
-import java.util.StringJoiner;
-import java.util.function.Consumer;
-
-/**
- * Utility class to help manage multiple callbacks based on different states.
- */
-public class MultiStateCallback {
-
-    private static final String TAG = "MultiStateCallback";
-    public static final boolean DEBUG_STATES = false;
-
-    private final SparseArray<Runnable> mCallbacks = new SparseArray<>();
-    private final SparseArray<Consumer<Boolean>> mStateChangeHandlers = new SparseArray<>();
-
-    private final String[] mStateNames;
-
-    public MultiStateCallback(String[] stateNames) {
-        mStateNames = DEBUG_STATES ? stateNames : null;
-    }
-
-    private int mState = 0;
-
-    /**
-     * Adds the provided state flags to the global state and executes any callbacks as a result.
-     */
-    public void setState(int stateFlag) {
-        if (DEBUG_STATES) {
-            Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
-                    + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
-        }
-
-        int oldState = mState;
-        mState = mState | stateFlag;
-
-        int count = mCallbacks.size();
-        for (int i = 0; i < count; i++) {
-            int state = mCallbacks.keyAt(i);
-
-            if ((mState & state) == state) {
-                Runnable callback = mCallbacks.valueAt(i);
-                if (callback != null) {
-                    // Set the callback to null, so that it does not run again.
-                    mCallbacks.setValueAt(i, null);
-                    callback.run();
-                }
-            }
-        }
-        notifyStateChangeHandlers(oldState);
-    }
-
-    /**
-     * Adds the provided state flags to the global state and executes any change handlers
-     * as a result.
-     */
-    public void clearState(int stateFlag) {
-        if (DEBUG_STATES) {
-            Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
-                    + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
-        }
-
-        int oldState = mState;
-        mState = mState & ~stateFlag;
-        notifyStateChangeHandlers(oldState);
-    }
-
-    private void notifyStateChangeHandlers(int oldState) {
-        int count = mStateChangeHandlers.size();
-        for (int i = 0; i < count; i++) {
-            int state = mStateChangeHandlers.keyAt(i);
-            boolean wasOn = (state & oldState) == state;
-            boolean isOn = (state & mState) == state;
-
-            if (wasOn != isOn) {
-                mStateChangeHandlers.valueAt(i).accept(isOn);
-            }
-        }
-    }
-
-    /**
-     * Sets the callbacks to be run when the provided states are enabled.
-     * The callback is only run once.
-     */
-    public void addCallback(int stateMask, Runnable callback) {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && mCallbacks.get(stateMask) != null) {
-            throw new IllegalStateException("Multiple callbacks on same state");
-        }
-        mCallbacks.put(stateMask, callback);
-    }
-
-    /**
-     * Sets the handler to be called when the provided states are enabled or disabled.
-     */
-    public void addChangeHandler(int stateMask, Consumer<Boolean> handler) {
-        mStateChangeHandlers.put(stateMask, handler);
-    }
-
-    public int getState() {
-        return mState;
-    }
-
-    public boolean hasStates(int stateMask) {
-        return (mState & stateMask) == stateMask;
-    }
-
-    private String convertToFlagNames(int flags) {
-        StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
-        for (int i = 0; i < mStateNames.length; i++) {
-            if ((flags & (1 << i)) != 0) {
-                joiner.add(mStateNames[i]);
-            }
-        }
-        return joiner.toString();
-    }
-
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index a8d402e..434a929 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -25,14 +25,16 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.ViewConfiguration;
 
-import com.android.launcher3.BaseDraggingActivity;
+import androidx.annotation.BinderThread;
+
+import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -46,37 +48,47 @@
 public class OverviewCommandHelper {
 
     private final Context mContext;
-    private final ActivityManagerWrapper mAM;
+    private final RecentsAnimationDeviceState mDeviceState;
     private final RecentsModel mRecentsModel;
     private final OverviewComponentObserver mOverviewComponentObserver;
 
     private long mLastToggleTime;
 
-    public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
+    public OverviewCommandHelper(Context context, RecentsAnimationDeviceState deviceState,
+            OverviewComponentObserver observer) {
         mContext = context;
-        mAM = ActivityManagerWrapper.getInstance();
+        mDeviceState = deviceState;
         mRecentsModel = RecentsModel.INSTANCE.get(mContext);
         mOverviewComponentObserver = observer;
     }
 
+    @BinderThread
     public void onOverviewToggle() {
         // If currently screen pinning, do not enter overview
-        if (mAM.isScreenPinningActive()) {
+        if (mDeviceState.isScreenPinningActive()) {
             return;
         }
 
-        mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+        ActivityManagerWrapper.getInstance()
+                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
     }
 
+    @BinderThread
     public void onOverviewShown(boolean triggeredFromAltTab) {
+        if (triggeredFromAltTab) {
+            ActivityManagerWrapper.getInstance()
+                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+        }
         MAIN_EXECUTOR.execute(new ShowRecentsCommand(triggeredFromAltTab));
     }
 
+    @BinderThread
     public void onOverviewHidden() {
         MAIN_EXECUTOR.execute(new HideRecentsCommand());
     }
 
+    @BinderThread
     public void onTip(int actionType, int viewType) {
         MAIN_EXECUTOR.execute(() ->
                 UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
@@ -93,14 +105,14 @@
         @Override
         protected boolean handleCommand(long elapsedTime) {
             // TODO: Go to the next page if started from alt-tab.
-            return mHelper.getVisibleRecentsView() != null;
+            return mActivityInterface.getVisibleRecentsView() != null;
         }
 
         @Override
         protected void onTransitionComplete() {
             // TODO(b/138729100) This doesn't execute first time launcher is run
             if (mTriggeredFromAltTab) {
-                RecentsView rv = (RecentsView) mHelper.getVisibleRecentsView();
+                RecentsView rv =  mActivityInterface.getVisibleRecentsView();
                 if (rv == null) {
                     return;
                 }
@@ -125,7 +137,7 @@
 
         @Override
         protected boolean handleCommand(long elapsedTime) {
-            RecentsView recents = (RecentsView) mHelper.getVisibleRecentsView();
+            RecentsView recents = mActivityInterface.getVisibleRecentsView();
             if (recents == null) {
                 return false;
             }
@@ -139,9 +151,9 @@
         }
     }
 
-    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
+    private class RecentsActivityCommand<T extends StatefulActivity<?>> implements Runnable {
 
-        protected final ActivityControlHelper<T> mHelper;
+        protected final BaseActivityInterface<?, T> mActivityInterface;
         private final long mCreateTime;
         private final AppToOverviewAnimationProvider<T> mAnimationProvider;
 
@@ -150,10 +162,10 @@
         private ActivityInitListener mListener;
 
         public RecentsActivityCommand() {
-            mHelper = mOverviewComponentObserver.getActivityControlHelper();
+            mActivityInterface = mOverviewComponentObserver.getActivityInterface();
             mCreateTime = SystemClock.elapsedRealtime();
-            mAnimationProvider =
-                    new AppToOverviewAnimationProvider<>(mHelper, RecentsModel.getRunningTaskId());
+            mAnimationProvider = new AppToOverviewAnimationProvider<>(mActivityInterface,
+                    RecentsModel.getRunningTaskId(), mDeviceState);
 
             // Preload the plan
             mRecentsModel.getTasks(null);
@@ -169,15 +181,23 @@
                 return;
             }
 
-            if (mHelper.switchToRecentsIfVisible(this::onTransitionComplete)) {
+            if (mActivityInterface.switchToRecentsIfVisible(this::onTransitionComplete)) {
                 // If successfully switched, then return
                 return;
             }
 
             // Otherwise, start overview.
-            mListener = mHelper.createActivityInitListener(this::onActivityReady);
+            mListener = mActivityInterface.createActivityInitListener(this::onActivityReady);
             mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
-                    this::createWindowAnimation, mContext, MAIN_EXECUTOR.getHandler(),
+                    new RemoteAnimationProvider() {
+                        @Override
+                        public AnimatorSet createWindowAnimation(
+                                RemoteAnimationTargetCompat[] appTargets,
+                                RemoteAnimationTargetCompat[] wallpaperTargets) {
+                            return RecentsActivityCommand.this.createWindowAnimation(appTargets,
+                                    wallpaperTargets);
+                        }
+                    }, mContext, MAIN_EXECUTOR.getHandler(),
                     mAnimationProvider.getRecentsLaunchDuration());
         }
 
@@ -185,7 +205,7 @@
             // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows
             //       the menu activity which takes window focus, preventing the right condition from
             //       being run below
-            RecentsView recents = mHelper.getVisibleRecentsView();
+            RecentsView recents = mActivityInterface.getVisibleRecentsView();
             if (recents != null) {
                 // Launch the next task
                 recents.showNextTask();
@@ -198,18 +218,24 @@
             return false;
         }
 
-        private boolean onActivityReady(T activity, Boolean wasVisible) {
+        private boolean onActivityReady(Boolean wasVisible) {
+            final T activity = mActivityInterface.getCreatedActivity();
             if (!mUserEventLogged) {
                 activity.getUserEventDispatcher().logActionCommand(
                         LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                        mHelper.getContainerType(),
+                        mActivityInterface.getContainerType(),
                         LauncherLogProto.ContainerType.TASKSWITCHER);
                 mUserEventLogged = true;
             }
+
+            // Switch prediction client to overview
+            PredictionUiStateManager.INSTANCE.get(activity).switchClient(
+                    PredictionUiStateManager.Client.OVERVIEW);
             return mAnimationProvider.onActivityReady(activity, wasVisible);
         }
 
-        private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
+        private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
+                RemoteAnimationTargetCompat[] wallpaperTargets) {
             if (LatencyTrackerCompat.isEnabled(mContext)) {
                 LatencyTrackerCompat.logToggleRecents(
                         (int) (SystemClock.uptimeMillis() - mToggleClickedTime));
@@ -217,7 +243,8 @@
 
             mListener.unregister();
 
-            AnimatorSet animatorSet = mAnimationProvider.createWindowAnimation(targetCompats);
+            AnimatorSet animatorSet = mAnimationProvider.createWindowAnimation(appTargets,
+                    wallpaperTargets);
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 3c78dd8..6f4d34c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -1,27 +1,38 @@
 package com.android.quickstep;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
+import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.views.RecentsView;
-
-import java.util.concurrent.ExecutionException;
 
 public class QuickstepTestInformationHandler extends TestInformationHandler {
 
+    protected final Context mContext;
+
     public QuickstepTestInformationHandler(Context context) {
+        mContext = context;
     }
 
     @Override
     public Bundle call(String method) {
         final Bundle response = new Bundle();
         switch (method) {
+            case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
+                return getLauncherUIProperty(Bundle::putInt, l -> {
+                    final float progress = LauncherState.OVERVIEW.getVerticalProgress(l)
+                            - LauncherState.ALL_APPS.getVerticalProgress(l);
+                    final float distance = l.getAllAppsController().getShiftRange() * progress;
+                    return (int) distance;
+                });
+            }
+
             case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
                         LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
@@ -31,43 +42,20 @@
 
             case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
-                        LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile);
+                        LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile,
+                                PagedOrientationHandler.PORTRAIT);
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
                 return response;
             }
 
             case TestProtocol.REQUEST_HOTSEAT_TOP: {
-                if (mLauncher == null) return null;
-
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                        PortraitStatesTouchController.getHotseatTop(mLauncher));
-                return response;
+                return getLauncherUIProperty(
+                        Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
             }
 
-            case TestProtocol.REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN: {
-                try {
-                    final int leftMargin = MAIN_EXECUTOR.submit(() ->
-                            mLauncher.<RecentsView>getOverviewPanel().getLeftGestureMargin()).get();
-                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, leftMargin);
-                } catch (ExecutionException e) {
-                    e.printStackTrace();
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-                return response;
-            }
-
-            case TestProtocol.REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN: {
-                try {
-                    final int rightMargin = MAIN_EXECUTOR.submit(() ->
-                            mLauncher.<RecentsView>getOverviewPanel().getRightGestureMargin()).
-                            get();
-                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, rightMargin);
-                } catch (ExecutionException e) {
-                    e.printStackTrace();
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
+            case TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
                 return response;
             }
         }
@@ -76,6 +64,18 @@
     }
 
     @Override
+    protected Activity getCurrentActivity() {
+        RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
+        OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
+        try {
+            return observer.getActivityInterface().getCreatedActivity();
+        } finally {
+            observer.onDestroy();
+            rads.destroy();
+        }
+    }
+
+    @Override
     protected boolean isLauncherInitialized() {
         return super.isLauncherInitialized() && TouchInteractionService.isInitialized();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 9bdc98b..6f2f9fb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -15,13 +15,15 @@
  */
 package com.android.quickstep;
 
+import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl
-        .STATUS_BAR_TRANSITION_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl
-        .STATUS_BAR_TRANSITION_PRE_DELAY;
-import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
+import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.Animator;
@@ -30,47 +32,73 @@
 import android.app.ActivityOptions;
 import android.content.Intent;
 import android.content.res.Configuration;
+import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.view.View;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAnimationRunner;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.fallback.FallbackRecentsStateController;
 import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.fallback.RecentsRootView;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.ObjectWrapper;
+import com.android.quickstep.fallback.RecentsDragLayer;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
 /**
  * A recents activity that shows the recently launched tasks as swipable task cards.
  * See {@link com.android.quickstep.views.RecentsView}.
  */
-public final class RecentsActivity extends BaseRecentsActivity {
+public final class RecentsActivity extends StatefulActivity<RecentsState> {
 
-    public static final String EXTRA_THUMBNAIL = "thumbnailData";
-    public static final String EXTRA_TASK_ID = "taskID";
+    public static final ActivityTracker<RecentsActivity> ACTIVITY_TRACKER =
+            new ActivityTracker<>();
 
     private Handler mUiHandler = new Handler(Looper.getMainLooper());
-    private RecentsRootView mRecentsRootView;
-    private FallbackRecentsView mFallbackRecentsView;
 
-    @Override
-    protected void initViews() {
-        setContentView(R.layout.fallback_recents_activity);
-        mRecentsRootView = findViewById(R.id.drag_layer);
+    private RecentsDragLayer mDragLayer;
+    private FallbackRecentsView mFallbackRecentsView;
+    private OverviewActionsView mActionsView;
+
+    private Configuration mOldConfig;
+
+    private StateManager<RecentsState> mStateManager;
+
+    /**
+     * Init drag layer and overview panel views.
+     */
+    protected void setupViews() {
+        inflateRootView(R.layout.fallback_recents_activity);
+        setContentView(getRootView());
+        mDragLayer = findViewById(R.id.drag_layer);
         mFallbackRecentsView = findViewById(R.id.overview_panel);
-        mRecentsRootView.setup();
+        mActionsView = findViewById(R.id.overview_actions_view);
+
+        mDragLayer.recreateControllers();
+        mFallbackRecentsView.init(mActionsView);
     }
 
     @Override
@@ -79,55 +107,45 @@
         super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
     }
 
-    public void onRootViewSizeChanged() {
-        if (isInMultiWindowMode()) {
-            onHandleConfigChanged();
-        }
-    }
-
     @Override
     protected void onNewIntent(Intent intent) {
-        if (intent.getExtras() != null) {
-            int taskID = intent.getIntExtra(EXTRA_TASK_ID, 0);
-            IBinder thumbnail = intent.getExtras().getBinder(EXTRA_THUMBNAIL);
-            if (taskID != 0 && thumbnail instanceof ObjectWrapper) {
-                ThumbnailData thumbnailData = ((ObjectWrapper<ThumbnailData>) thumbnail).get();
-                mFallbackRecentsView.showCurrentTask(taskID);
-                mFallbackRecentsView.updateThumbnail(taskID, thumbnailData);
-            }
-        }
-        intent.removeExtra(EXTRA_TASK_ID);
-        intent.removeExtra(EXTRA_THUMBNAIL);
         super.onNewIntent(intent);
+        ACTIVITY_TRACKER.handleNewIntent(this, intent);
     }
 
-    @Override
+    /**
+         * Logic for when device configuration changes (rotation, screen size change, multi-window,
+         * etc.)
+         */
     protected void onHandleConfigChanged() {
-        super.onHandleConfigChanged();
-        mRecentsRootView.setup();
+        mUserEventDispatcher = null;
+        initDeviceProfile();
+
+        AbstractFloatingView.closeOpenViews(this, true,
+                AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
+        dispatchDeviceProfileChanged();
+
+        reapplyUi();
+        mDragLayer.recreateControllers();
     }
 
-    @Override
-    protected void reapplyUi() {
-        mRecentsRootView.dispatchInsets();
-    }
-
-    @Override
+    /**
+     * Generate the device profile to use in this activity.
+     * @return device profile
+     */
     protected DeviceProfile createDeviceProfile() {
         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
-        return (mRecentsRootView != null) && isInMultiWindowMode()
-                ? dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize())
-                : super.createDeviceProfile();
+
+        // In case we are reusing IDP, create a copy so that we don't conflict with Launcher
+        // activity.
+        return (mDragLayer != null) && isInMultiWindowMode()
+                ? dp.getMultiWindowProfile(this, getMultiWindowDisplaySize())
+                : dp.copy(this);
     }
 
     @Override
     public BaseDragLayer getDragLayer() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public View getRootView() {
-        return mRecentsRootView;
+        return mDragLayer;
     }
 
     @Override
@@ -135,6 +153,10 @@
         return (T) mFallbackRecentsView;
     }
 
+    public OverviewActionsView getActionsView() {
+        return mActionsView;
+    }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
@@ -152,15 +174,11 @@
                 true /* startAtFrontOfQueue */) {
 
             @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
-                    AnimationResult result) {
-                AnimatorSet anim = composeRecentsLaunchAnimator(taskView, targetCompats);
-                anim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mFallbackRecentsView.resetViewUI();
-                    }
-                });
+            public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+                    RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+                AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+                        wallpaperTargets);
+                anim.addListener(resetStateListener());
                 result.setAnimation(anim, RecentsActivity.this);
             }
         };
@@ -174,25 +192,22 @@
      * Composes the animations for a launch from the recents list if possible.
      */
     private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
-            RemoteAnimationTargetCompat[] targets) {
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets) {
         AnimatorSet target = new AnimatorSet();
-        boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING);
-        ClipAnimationHelper helper = new ClipAnimationHelper(this);
-        target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper)
-                .setDuration(RECENTS_LAUNCH_DURATION));
+        boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
+        PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+        createRecentsWindowAnimator(taskView, !activityClosing, appTargets,
+                wallpaperTargets, null /* depthController */, pa);
+        target.play(pa.buildAnim());
 
         // Found a visible recents task that matches the opening app, lets launch the app from there
         if (activityClosing) {
             Animator adjacentAnimation = mFallbackRecentsView
-                    .createAdjacentPageAnimForTaskLaunch(taskView, helper);
+                    .createAdjacentPageAnimForTaskLaunch(taskView);
             adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
             adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
-            adjacentAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mFallbackRecentsView.resetTaskVisuals();
-                }
-            });
+            adjacentAnimation.addListener(resetStateListener());
             target.play(adjacentAnimation);
         }
         return target;
@@ -204,16 +219,118 @@
         // onActivityStart callback.
         mFallbackRecentsView.setContentAlpha(1);
         super.onStart();
-        mFallbackRecentsView.resetTaskVisuals();
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        mFallbackRecentsView.reset();
+
+        // Workaround for b/78520668, explicitly trim memory once UI is hidden
+        onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
     }
 
-    public void onTaskLaunched() {
-        mFallbackRecentsView.resetTaskVisuals();
+    @Override
+    protected void onResume() {
+        super.onResume();
+        AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mStateManager = new StateManager<>(this, RecentsState.DEFAULT);
+
+        mOldConfig = new Configuration(getResources().getConfiguration());
+        initDeviceProfile();
+        setupViews();
+
+        getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
+                Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
+        ACTIVITY_TRACKER.handleCreate(this);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        int diff = newConfig.diff(mOldConfig);
+        if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
+            onHandleConfigChanged();
+        }
+        mOldConfig.setTo(newConfig);
+        super.onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * Initialize/update the device profile.
+     */
+    private void initDeviceProfile() {
+        mDeviceProfile = createDeviceProfile();
+        onDeviceProfileInitiated();
+    }
+
+    @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
+        // overview
+        RecentsModel.INSTANCE.get(this).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        RecentsModel.INSTANCE.get(this).onTrimMemory(level);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        ACTIVITY_TRACKER.onActivityDestroyed(this);
+    }
+
+    @Override
+    public void onBackPressed() {
+        // TODO: Launch the task we came from
+        startHome();
+    }
+
+    public void startHome() {
+        startActivity(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+    }
+
+    @Override
+    protected StateHandler<RecentsState>[] createStateHandlers() {
+        return new StateHandler[] { new FallbackRecentsStateController(this) };
+    }
+
+    @Override
+    public StateManager<RecentsState> getStateManager() {
+        return mStateManager;
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        super.dump(prefix, fd, writer, args);
+        writer.println(prefix + "Misc:");
+        dumpMisc(prefix + "\t", writer);
+    }
+
+    @Override
+    public AtomicAnimationFactory<RecentsState> createAtomicAnimationFactory() {
+        return new RecentsAtomicAnimationFactory<>(this, 0);
+    }
+
+    private AnimatorListenerAdapter resetStateListener() {
+        return new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mFallbackRecentsView.resetTaskVisuals();
+                mStateManager.reapplyState();
+            }
+        };
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
deleted file mode 100644
index 784c956..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_UP;
-
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.inputconsumers.InputConsumer;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.systemui.shared.system.InputConsumerController;
-
-import java.util.ArrayList;
-import java.util.function.Supplier;
-
-/**
- * Wrapper around RecentsAnimationController to help with some synchronization
- */
-public class RecentsAnimationWrapper {
-
-    private static final String TAG = "RecentsAnimationWrapper";
-
-    // A list of callbacks to run when we receive the recents animation target. There are different
-    // than the state callbacks as these run on the current worker thread.
-    private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
-
-    public SwipeAnimationTargetSet targetSet;
-
-    private boolean mWindowThresholdCrossed = false;
-
-    private final InputConsumerController mInputConsumerController;
-    private final Supplier<InputConsumer> mInputProxySupplier;
-
-    private InputConsumer mInputConsumer;
-    private boolean mTouchInProgress;
-
-    private boolean mFinishPending;
-
-    public RecentsAnimationWrapper(InputConsumerController inputConsumerController,
-            Supplier<InputConsumer> inputProxySupplier) {
-        mInputConsumerController = inputConsumerController;
-        mInputProxySupplier = inputProxySupplier;
-    }
-
-    public boolean hasTargets() {
-        return targetSet != null && targetSet.hasTargets();
-    }
-
-    @UiThread
-    public synchronized void setController(SwipeAnimationTargetSet targetSet) {
-        Preconditions.assertUIThread();
-        this.targetSet = targetSet;
-
-        if (targetSet == null) {
-            return;
-        }
-        targetSet.setWindowThresholdCrossed(mWindowThresholdCrossed);
-
-        if (!mCallbacks.isEmpty()) {
-            for (Runnable action : new ArrayList<>(mCallbacks)) {
-                action.run();
-            }
-            mCallbacks.clear();
-        }
-    }
-
-    public synchronized void runOnInit(Runnable action) {
-        if (targetSet == null) {
-            mCallbacks.add(action);
-        } else {
-            action.run();
-        }
-    }
-
-    /** See {@link #finish(boolean, Runnable, boolean)} */
-    @UiThread
-    public void finish(boolean toRecents, Runnable onFinishComplete) {
-        finish(toRecents, onFinishComplete, false /* sendUserLeaveHint */);
-    }
-
-    /**
-     * @param onFinishComplete A callback that runs on the main thread after the animation
-     *                         controller has finished on the background thread.
-     * @param sendUserLeaveHint Determines whether userLeaveHint flag will be set on the pausing
-     *                          activity. If userLeaveHint is true, the activity will enter into
-     *                          picture-in-picture mode upon being paused.
-     */
-    @UiThread
-    public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
-        Preconditions.assertUIThread();
-        if (!toRecents) {
-            finishAndClear(false, onFinishComplete, sendUserLeaveHint);
-        } else {
-            if (mTouchInProgress) {
-                mFinishPending = true;
-                // Execute the callback
-                if (onFinishComplete != null) {
-                    onFinishComplete.run();
-                }
-            } else {
-                finishAndClear(true, onFinishComplete, sendUserLeaveHint);
-            }
-        }
-    }
-
-    private void finishAndClear(boolean toRecents, Runnable onFinishComplete,
-            boolean sendUserLeaveHint) {
-        SwipeAnimationTargetSet controller = targetSet;
-        targetSet = null;
-        disableInputProxy();
-        if (controller != null) {
-            controller.finishController(toRecents, onFinishComplete, sendUserLeaveHint);
-        }
-    }
-
-    public void enableInputConsumer() {
-        if (targetSet != null) {
-            targetSet.enableInputConsumer();
-        }
-    }
-
-    /**
-     * Indicates that the gesture has crossed the window boundary threshold and system UI can be
-     * update the represent the window behind
-     */
-    public void setWindowThresholdCrossed(boolean windowThresholdCrossed) {
-        if (mWindowThresholdCrossed != windowThresholdCrossed) {
-            mWindowThresholdCrossed = windowThresholdCrossed;
-            if (targetSet != null) {
-                targetSet.setWindowThresholdCrossed(windowThresholdCrossed);
-            }
-        }
-    }
-
-    public void enableInputProxy() {
-        mInputConsumerController.setInputListener(this::onInputConsumerEvent);
-    }
-
-    private void disableInputProxy() {
-        if (mInputConsumer != null && mTouchInProgress) {
-            long now = SystemClock.uptimeMillis();
-            MotionEvent tempCancel = MotionEvent.obtain(now,  now, ACTION_CANCEL, 0, 0, 0);
-            mInputConsumer.onMotionEvent(tempCancel);
-            tempCancel.recycle();
-        }
-        mInputConsumerController.setInputListener(null);
-    }
-
-    private boolean onInputConsumerEvent(InputEvent ev) {
-        if (ev instanceof MotionEvent) {
-            onInputConsumerMotionEvent((MotionEvent) ev);
-        } else if (ev instanceof KeyEvent) {
-            if (mInputConsumer == null) {
-                mInputConsumer = mInputProxySupplier.get();
-            }
-            mInputConsumer.onKeyEvent((KeyEvent) ev);
-            return true;
-        }
-        return false;
-    }
-
-    private boolean onInputConsumerMotionEvent(MotionEvent ev) {
-        int action = ev.getAction();
-
-        // Just to be safe, verify that ACTION_DOWN comes before any other action,
-        // and ignore any ACTION_DOWN after the first one (though that should not happen).
-        if (!mTouchInProgress && action != ACTION_DOWN) {
-            Log.w(TAG, "Received non-down motion before down motion: " + action);
-            return false;
-        }
-        if (mTouchInProgress && action == ACTION_DOWN) {
-            Log.w(TAG, "Received down motion while touch was already in progress");
-            return false;
-        }
-
-        if (action == ACTION_DOWN) {
-            mTouchInProgress = true;
-            if (mInputConsumer == null) {
-                mInputConsumer = mInputProxySupplier.get();
-            }
-        } else if (action == ACTION_CANCEL || action == ACTION_UP) {
-            // Finish any pending actions
-            mTouchInProgress = false;
-            if (mFinishPending) {
-                mFinishPending = false;
-                finishAndClear(true /* toRecents */, null, false /* sendUserLeaveHint */);
-            }
-        }
-        if (mInputConsumer != null) {
-            mInputConsumer.onMotionEvent(ev);
-        }
-
-        return true;
-    }
-
-    public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
-        if (targetSet != null) {
-            targetSet.controller.setDeferCancelUntilNextTransition(defer, screenshot);
-        }
-    }
-
-    public SwipeAnimationTargetSet getController() {
-        return targetSet;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
deleted file mode 100644
index 8783ee3..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.util.Log;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.RecentsAnimationListenerSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
-
-import java.io.PrintWriter;
-
-/**
- * Utility class used to store state information shared across multiple transitions.
- */
-public class SwipeSharedState implements SwipeAnimationListener {
-
-    private OverviewComponentObserver mOverviewComponentObserver;
-
-    private RecentsAnimationListenerSet mRecentsAnimationListener;
-    private SwipeAnimationTargetSet mLastAnimationTarget;
-
-    private boolean mLastAnimationCancelled = false;
-    private boolean mLastAnimationRunning = false;
-
-    public boolean canGestureBeContinued;
-    public boolean goingToLauncher;
-    public boolean recentsAnimationFinishInterrupted;
-    public int nextRunningTaskId = -1;
-    private int mLogId;
-
-    public void setOverviewComponentObserver(OverviewComponentObserver observer) {
-        mOverviewComponentObserver = observer;
-    }
-
-    @Override
-    public final void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        mLastAnimationTarget = targetSet;
-
-        mLastAnimationCancelled = false;
-        mLastAnimationRunning = true;
-    }
-
-    private void clearAnimationTarget() {
-        if (mLastAnimationTarget != null) {
-            mLastAnimationTarget.release();
-            mLastAnimationTarget = null;
-        }
-    }
-
-    @Override
-    public final void onRecentsAnimationCanceled() {
-        clearAnimationTarget();
-
-        mLastAnimationCancelled = true;
-        mLastAnimationRunning = false;
-    }
-
-    private void clearListenerState(boolean finishAnimation) {
-        if (mRecentsAnimationListener != null) {
-            mRecentsAnimationListener.removeListener(this);
-            mRecentsAnimationListener.cancelListener();
-            if (mLastAnimationRunning && mLastAnimationTarget != null) {
-                Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
-                        finishAnimation
-                                ? mLastAnimationTarget::finishAnimation
-                                : mLastAnimationTarget::cancelAnimation);
-                mLastAnimationTarget = null;
-            }
-        }
-        mRecentsAnimationListener = null;
-        clearAnimationTarget();
-        mLastAnimationCancelled = false;
-        mLastAnimationRunning = false;
-    }
-
-    private void onSwipeAnimationFinished(SwipeAnimationTargetSet targetSet) {
-        if (mLastAnimationTarget == targetSet) {
-            mLastAnimationRunning = false;
-        }
-    }
-
-    public RecentsAnimationListenerSet newRecentsAnimationListenerSet() {
-        Preconditions.assertUIThread();
-
-        if (mLastAnimationRunning) {
-            String msg = "New animation started before completing old animation";
-            if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                throw new IllegalArgumentException(msg);
-            } else {
-                Log.e("SwipeSharedState", msg, new Exception());
-            }
-        }
-
-        clearListenerState(false /* finishAnimation */);
-        boolean shouldMinimiseSplitScreen = mOverviewComponentObserver == null ? false
-                : mOverviewComponentObserver.getActivityControlHelper().shouldMinimizeSplitScreen();
-        mRecentsAnimationListener = new RecentsAnimationListenerSet(
-                shouldMinimiseSplitScreen, this::onSwipeAnimationFinished);
-        mRecentsAnimationListener.addListener(this);
-        return mRecentsAnimationListener;
-    }
-
-    public RecentsAnimationListenerSet getActiveListener() {
-        return mRecentsAnimationListener;
-    }
-
-    public void applyActiveRecentsAnimationState(SwipeAnimationListener listener) {
-        if (mLastAnimationTarget != null) {
-            listener.onRecentsAnimationStart(mLastAnimationTarget);
-        } else if (mLastAnimationCancelled) {
-            listener.onRecentsAnimationCanceled();
-        }
-    }
-
-    /**
-     * Called when a recents animation has finished, but was interrupted before the next task was
-     * launched. The given {@param runningTaskId} should be used as the running task for the
-     * continuing input consumer.
-     */
-    public void setRecentsAnimationFinishInterrupted(int runningTaskId) {
-        recentsAnimationFinishInterrupted = true;
-        nextRunningTaskId = runningTaskId;
-        mLastAnimationTarget = mLastAnimationTarget.cloneWithoutTargets();
-    }
-
-    public void clearAllState(boolean finishAnimation) {
-        clearListenerState(finishAnimation);
-        canGestureBeContinued = false;
-        recentsAnimationFinishInterrupted = false;
-        nextRunningTaskId = -1;
-        goingToLauncher = false;
-    }
-
-    public void dump(String prefix, PrintWriter pw) {
-        pw.println(prefix + "goingToLauncher=" + goingToLauncher);
-        pw.println(prefix + "canGestureBeContinued=" + canGestureBeContinued);
-        pw.println(prefix + "recentsAnimationFinishInterrupted=" + recentsAnimationFinishInterrupted);
-        pw.println(prefix + "nextRunningTaskId=" + nextRunningTaskId);
-        pw.println(prefix + "lastAnimationCancelled=" + mLastAnimationCancelled);
-        pw.println(prefix + "lastAnimationRunning=" + mLastAnimationRunning);
-        pw.println(prefix + "logTraceId=" + mLogId);
-    }
-
-    public void setLogTraceId(int logId) {
-        this.mLogId = logId;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
new file mode 100644
index 0000000..dc8f1c5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
+
+public abstract class SwipeUpAnimationLogic {
+
+    protected static final Rect TEMP_RECT = new Rect();
+    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
+
+    protected DeviceProfile mDp;
+
+    protected final Context mContext;
+    protected final RecentsAnimationDeviceState mDeviceState;
+    protected final GestureState mGestureState;
+    protected final TaskViewSimulator mTaskViewSimulator;
+
+    protected final TransformParams mTransformParams;
+
+    // Shift in the range of [0, 1].
+    // 0 => preview snapShot is completely visible, and hotseat is completely translated down
+    // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
+    // visible.
+    protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+
+    // The distance needed to drag to reach the task size in recents.
+    protected int mTransitionDragLength;
+    // How much further we can drag past recents, as a factor of mTransitionDragLength.
+    protected float mDragLengthFactor = 1;
+    // Start resisting when swiping past this factor of mTransitionDragLength.
+    private float mDragLengthFactorStartPullback = 1f;
+    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
+    private float mDragLengthFactorMaxPullback = 1f;
+
+    protected AnimatorPlaybackController mWindowTransitionController;
+
+    public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, TransformParams transformParams) {
+        mContext = context;
+        mDeviceState = deviceState;
+        mGestureState = gestureState;
+        mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
+        mTransformParams = transformParams;
+
+        mTaskViewSimulator.setLayoutRotation(
+                mDeviceState.getCurrentActiveRotation(), mDeviceState.getDisplayRotation());
+    }
+
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        mDp = dp;
+
+        mTaskViewSimulator.setDp(dp);
+        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;
+
+            float startScale = mTaskViewSimulator.getFullScreenScale();
+            // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
+            mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
+            mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
+        } else {
+            mDragLengthFactor = 1;
+            mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
+        }
+
+        PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
+        mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
+        mWindowTransitionController = pa.createPlaybackController();
+    }
+
+    @UiThread
+    public void updateDisplacement(float displacement) {
+        // We are moving in the negative x/y direction
+        displacement = -displacement;
+        float shift;
+        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
+            shift = mDragLengthFactor;
+        } else {
+            float translation = Math.max(displacement, 0);
+            shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+            if (shift > mDragLengthFactorStartPullback) {
+                float pullbackProgress = Utilities.getProgress(shift,
+                        mDragLengthFactorStartPullback, mDragLengthFactor);
+                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
+                shift = mDragLengthFactorStartPullback + pullbackProgress
+                        * (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
+            }
+        }
+
+        mCurrentShift.updateValue(shift);
+    }
+
+    /**
+     * Called when the value of {@link #mCurrentShift} changes
+     */
+    @UiThread
+    public abstract void updateFinalShift();
+
+    protected PagedOrientationHandler getOrientationHandler() {
+        return mTaskViewSimulator.getOrientationState().getOrientationHandler();
+    }
+
+    protected abstract class HomeAnimationFactory {
+
+        public FloatingIconView mIconView;
+
+        public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
+            mIconView = iconView;
+        }
+
+        public @NonNull RectF getWindowTargetRect() {
+            PagedOrientationHandler orientationHandler = getOrientationHandler();
+            DeviceProfile dp = mDp;
+            final int halfIconSize = dp.iconSizePx / 2;
+            float primaryDimension = orientationHandler
+                    .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx);
+            float secondaryDimension = orientationHandler
+                    .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx);
+            final float targetX =  primaryDimension / 2f;
+            final float targetY = secondaryDimension - dp.hotseatBarSizePx;
+            // Fallback to animate to center of screen.
+            return new RectF(targetX - halfIconSize, targetY - halfIconSize,
+                    targetX + halfIconSize, targetY + halfIconSize);
+        }
+
+        public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome();
+
+        public void playAtomicAnimation(float velocity) {
+            // No-op
+        }
+    }
+
+    /**
+     * Creates an animation that transforms the current app window into the home app.
+     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+     * @param homeAnimationFactory The home animation factory.
+     */
+    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+            HomeAnimationFactory homeAnimationFactory) {
+        final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
+        final FloatingIconView fiv = homeAnimationFactory.mIconView;
+        final boolean isFloatingIconView = fiv != null;
+
+        mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
+        mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
+        RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+
+        // Matrix to map a rect in Launcher space to window space
+        Matrix homeToWindowPositionMap = new Matrix();
+        mTaskViewSimulator.applyWindowToHomeRotation(homeToWindowPositionMap);
+
+        final RectF startRect = new RectF(cropRectF);
+        mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
+        // Move the startRect to Launcher space as floatingIconView runs in Launcher
+        Matrix windowToHomePositionMap = new Matrix();
+        homeToWindowPositionMap.invert(windowToHomePositionMap);
+        windowToHomePositionMap.mapRect(startRect);
+
+        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
+        if (isFloatingIconView) {
+            anim.addAnimatorListener(fiv);
+            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+            fiv.setFastFinishRunnable(anim::end);
+        }
+
+        SpringAnimationRunner runner = new SpringAnimationRunner(
+                homeAnimationFactory, cropRectF, homeToWindowPositionMap);
+        anim.addOnUpdateListener(runner);
+        anim.addAnimatorListener(runner);
+        return anim;
+    }
+
+    /**
+     * @param progress The progress of the animation to the home screen.
+     * @return The current alpha to set on the animating app window.
+     */
+    protected float getWindowAlpha(float progress) {
+        // Alpha interpolates between [1, 0] between progress values [start, end]
+        final float start = 0f;
+        final float end = 0.85f;
+
+        if (progress <= start) {
+            return 1f;
+        }
+        if (progress >= end) {
+            return 0f;
+        }
+        return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
+    }
+
+    protected class SpringAnimationRunner extends AnimationSuccessListener
+            implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
+
+        final Rect mCropRect = new Rect();
+        final Matrix mMatrix = new Matrix();
+
+        final RectF mWindowCurrentRect = new RectF();
+        final Matrix mHomeToWindowPositionMap;
+
+        final FloatingIconView mFIV;
+        final AnimatorPlaybackController mHomeAnim;
+        final RectF mCropRectF;
+
+        final float mStartRadius;
+        final float mEndRadius;
+        final float mWindowAlphaThreshold;
+
+        SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
+                Matrix homeToWindowPositionMap) {
+            mHomeAnim = factory.createActivityAnimationToHome();
+            mCropRectF = cropRectF;
+            mHomeToWindowPositionMap = homeToWindowPositionMap;
+
+            cropRectF.roundOut(mCropRect);
+            mFIV = factory.mIconView;
+
+            // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
+            // rounding at the end of the animation.
+            mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
+            mEndRadius = cropRectF.width() / 2f;
+
+            // We want the window alpha to be 0 once this threshold is met, so that the
+            // FolderIconView can be seen morphing into the icon shape.
+            mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
+        }
+
+        @Override
+        public void onUpdate(RectF currentRect, float progress) {
+            mHomeAnim.setPlayFraction(progress);
+            mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
+
+            mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
+            float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
+            mTransformParams
+                    .setTargetAlpha(getWindowAlpha(progress))
+                    .setCornerRadius(cornerRadius);
+
+            mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
+            if (mFIV != null) {
+                mFIV.update(currentRect, 1f, progress,
+                        mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
+            }
+        }
+
+        @Override
+        public void onBuildTargetParams(
+                Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+            builder.withMatrix(mMatrix)
+                    .withWindowCrop(mCropRect)
+                    .withCornerRadius(params.getCornerRadius());
+        }
+
+        @Override
+        public void onCancel() {
+            if (mFIV != null) {
+                mFIV.fastFinish();
+            }
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            mHomeAnim.dispatchOnStart();
+        }
+
+        @Override
+        public void onAnimationSuccess(Animator animator) {
+            mHomeAnim.getAnimationPlayer().end();
+        }
+    }
+
+    public interface RunningWindowAnim {
+        void end();
+
+        void cancel();
+
+        static RunningWindowAnim wrap(Animator animator) {
+            return new RunningWindowAnim() {
+                @Override
+                public void end() {
+                    animator.end();
+                }
+
+                @Override
+                public void cancel() {
+                    animator.cancel();
+                }
+            };
+        }
+
+        static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
+            return new RunningWindowAnim() {
+                @Override
+                public void end() {
+                    rectFSpringAnim.end();
+                }
+
+                @Override
+                public void cancel() {
+                    rectFSpringAnim.cancel();
+                }
+            };
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index 17457aa..e9614d1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -16,18 +16,35 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static android.view.Surface.ROTATION_0;
 
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Insets;
 import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
 import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.TaskThumbnailView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -39,45 +56,213 @@
  */
 public class TaskOverlayFactory implements ResourceBasedOverride {
 
-    /** Note that these will be shown in order from top to bottom, if available for the task. */
-    private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
-            new TaskSystemShortcut.AppInfo(),
-            new TaskSystemShortcut.SplitScreen(),
-            new TaskSystemShortcut.Pin(),
-            new TaskSystemShortcut.Install(),
-            new TaskSystemShortcut.Freeform()
-    };
-
-    public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
-            forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
-
-    public List<TaskSystemShortcut> getEnabledShortcuts(TaskView taskView) {
-        final ArrayList<TaskSystemShortcut> shortcuts = new ArrayList<>();
+    public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
+        final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
         final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
-        for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
-            View.OnClickListener onClickListener =
-                    menuOption.getOnClickListener(activity, taskView);
-            if (onClickListener != null) {
-                shortcuts.add(menuOption);
+        for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
+            SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
+            if (shortcut != null) {
+                shortcuts.add(shortcut);
+            }
+        }
+        RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState();
+        boolean canLauncherRotate = orientedState.canRecentsActivityRotate();
+        boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0;
+
+        // Add overview actions to the menu when in in-place rotate landscape mode.
+        if (!canLauncherRotate && isInLandscape) {
+            // Add screenshot action to task menu.
+            SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT
+                    .getShortcut(activity, taskView);
+            if (screenshotShortcut != null) {
+                shortcuts.add(screenshotShortcut);
+            }
+
+            // Add modal action only if display orientation is the same as the device orientation.
+            if (orientedState.getDisplayRotation() == ROTATION_0) {
+                SystemShortcut modalShortcut = TaskShortcutFactory.MODAL
+                        .getShortcut(activity, taskView);
+                if (modalShortcut != null) {
+                    shortcuts.add(modalShortcut);
+                }
             }
         }
         return shortcuts;
     }
 
-    public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
-        return new TaskOverlay();
+    public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
+            forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
+
+    /**
+     * @return a launcher-provided OverscrollPlugin if available, otherwise null
+     */
+    public OverscrollPlugin getLocalOverscrollPlugin() {
+        return null;
     }
 
-    public static class TaskOverlay {
+    public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
+        return new TaskOverlay(thumbnailView);
+    }
+
+    /** Note that these will be shown in order from top to bottom, if available for the task. */
+    private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
+            TaskShortcutFactory.APP_INFO,
+            TaskShortcutFactory.SPLIT_SCREEN,
+            TaskShortcutFactory.PIN,
+            TaskShortcutFactory.INSTALL,
+            TaskShortcutFactory.FREE_FORM,
+            TaskShortcutFactory.WELLBEING
+    };
+
+    /**
+     * Overlay on each task handling Overview Action Buttons.
+     */
+    public static class TaskOverlay<T extends OverviewActionsView> {
+
+        private final Context mApplicationContext;
+        protected final TaskThumbnailView mThumbnailView;
+
+        private T mActionsView;
+        private ImageActionsApi mImageApi;
+        private boolean mIsAllowedByPolicy;
+
+        protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
+            mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
+            mThumbnailView = taskThumbnailView;
+            mImageApi = new ImageActionsApi(
+                mApplicationContext, mThumbnailView::getThumbnail);
+        }
+
+        protected T getActionsView() {
+            if (mActionsView == null) {
+                mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
+                        R.id.overview_actions_view);
+            }
+            return mActionsView;
+        }
 
         /**
          * Called when the current task is interactive for the user
          */
-        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix) { }
+        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+                boolean rotated) {
+            final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
+
+            getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+
+            getActionsView().setCallbacks(new OverlayUICallbacks() {
+                @Override
+                public void onShare() {
+                    if (isAllowedByPolicy) {
+                        mImageApi.startShareActivity();
+                    } else {
+                        showBlockedByPolicyMessage();
+                    }
+                }
+
+                @SuppressLint("NewApi")
+                @Override
+                public void onScreenshot() {
+                    saveScreenshot(task);
+                }
+            });
+        }
+
+        /**
+         * Called to save screenshot of the task thumbnail.
+         */
+        @SuppressLint("NewApi")
+        private void saveScreenshot(Task task) {
+            if (mThumbnailView.isRealSnapshot()) {
+                mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
+                        getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
+            } else {
+                showBlockedByPolicyMessage();
+            }
+        }
 
         /**
          * Called when the overlay is no longer used.
          */
-        public void reset() { }
+        public void reset() {
+        }
+
+        /**
+         * Called when the system wants to reset the modal visuals.
+         */
+        public void resetModalVisuals() {
+        }
+
+        /**
+         * Gets the modal state system shortcut.
+         */
+        public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) {
+            return null;
+        }
+
+        /**
+         * Gets the system shortcut for the screenshot that will be added to the task menu.
+         */
+        public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity,
+                ItemInfo iteminfo) {
+            return new ScreenshotSystemShortcut(activity, iteminfo);
+        }
+        /**
+         * Gets the task snapshot as it is displayed on the screen.
+         *
+         * @return the bounds of the snapshot in screen coordinates.
+         */
+        public Rect getTaskSnapshotBounds() {
+            int[] location = new int[2];
+            mThumbnailView.getLocationOnScreen(location);
+
+            return new Rect(location[0], location[1], mThumbnailView.getWidth() + location[0],
+                    mThumbnailView.getHeight() + location[1]);
+        }
+
+        /**
+         * Gets the insets that the snapshot is drawn with.
+         *
+         * @return the insets in screen coordinates.
+         */
+        @RequiresApi(api = Build.VERSION_CODES.Q)
+        public Insets getTaskSnapshotInsets() {
+            return mThumbnailView.getScaledInsets();
+        }
+
+        private void showBlockedByPolicyMessage() {
+            Toast.makeText(
+                    mThumbnailView.getContext(),
+                    R.string.blocked_by_policy,
+                    Toast.LENGTH_LONG).show();
+        }
+
+        private class ScreenshotSystemShortcut extends SystemShortcut {
+
+            private final BaseDraggingActivity mActivity;
+
+            ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) {
+                super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo);
+                mActivity = activity;
+            }
+
+            @Override
+            public void onClick(View view) {
+                saveScreenshot(mThumbnailView.getTaskView().getTask());
+                dismissTaskMenuView(mActivity);
+            }
+        }
+    }
+
+    /**
+     * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
+     * controller.
+     */
+    public interface OverlayUICallbacks {
+        /** User has indicated they want to share the current task. */
+        void onShare();
+
+        /** User has indicated they want to screenshot the current task. */
+        void onScreenshot();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
new file mode 100644
index 0000000..ff051b6
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
@@ -0,0 +1,327 @@
+/*
+ * 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.quickstep;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.popup.SystemShortcut.AppInfo;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityCompat;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public interface TaskShortcutFactory {
+
+    SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
+
+    TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo());
+
+    abstract class MultiWindowFactory implements TaskShortcutFactory {
+
+        private final int mIconRes;
+        private final int mTextRes;
+        private final LauncherEvent mLauncherEvent;
+
+        MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) {
+            mIconRes = iconRes;
+            mTextRes = textRes;
+            mLauncherEvent = launcherEvent;
+        }
+
+        protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
+        protected abstract ActivityOptions makeLaunchOptions(Activity activity);
+        protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
+
+        @Override
+        public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+            final Task task  = taskView.getTask();
+            if (!task.isDockable) {
+                return null;
+            }
+            if (!isAvailable(activity, task.key.displayId)) {
+                return null;
+            }
+            return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this,
+                    mLauncherEvent);
+        }
+    }
+
+    class MultiWindowSystemShortcut extends SystemShortcut {
+
+        private Handler mHandler;
+
+        private final RecentsView mRecentsView;
+        private final TaskThumbnailView mThumbnailView;
+        private final TaskView mTaskView;
+        private final MultiWindowFactory mFactory;
+        private final LauncherEvent mLauncherEvent;
+
+        public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
+                TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent) {
+            super(iconRes, textRes, activity, taskView.getItemInfo());
+            mLauncherEvent = launcherEvent;
+            mHandler = new Handler(Looper.getMainLooper());
+            mTaskView = taskView;
+            mRecentsView = activity.getOverviewPanel();
+            mThumbnailView = taskView.getThumbnail();
+            mFactory = factory;
+        }
+
+        @Override
+        public void onClick(View view) {
+            Task.TaskKey taskKey = mTaskView.getTask().key;
+            final int taskId = taskKey.id;
+
+            final View.OnLayoutChangeListener onLayoutChangeListener =
+                    new View.OnLayoutChangeListener() {
+                        @Override
+                        public void onLayoutChange(View v, int l, int t, int r, int b,
+                                int oldL, int oldT, int oldR, int oldB) {
+                            mTaskView.getRootView().removeOnLayoutChangeListener(this);
+                            mRecentsView.clearIgnoreResetTask(taskId);
+
+                            // Start animating in the side pages once launcher has been resized
+                            mRecentsView.dismissTask(mTaskView, false, false);
+                        }
+                    };
+
+            final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
+                    new DeviceProfile.OnDeviceProfileChangeListener() {
+                        @Override
+                        public void onDeviceProfileChanged(DeviceProfile dp) {
+                            mTarget.removeOnDeviceProfileChangeListener(this);
+                            if (dp.isMultiWindowMode) {
+                                mTaskView.getRootView().addOnLayoutChangeListener(
+                                        onLayoutChangeListener);
+                            }
+                        }
+                    };
+
+            dismissTaskMenuView(mTarget);
+
+            ActivityOptions options = mFactory.makeLaunchOptions(mTarget);
+            if (options != null
+                    && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+                            options)) {
+                if (!mFactory.onActivityStarted(mTarget)) {
+                    return;
+                }
+                // Add a device profile change listener to kick off animating the side tasks
+                // once we enter multiwindow mode and relayout
+                mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
+
+                final Runnable animStartedListener = () -> {
+                    // Hide the task view and wait for the window to be resized
+                    // TODO: Consider animating in launcher and do an in-place start activity
+                    //       afterwards
+                    mRecentsView.setIgnoreResetTask(taskId);
+                    mTaskView.setAlpha(0f);
+                };
+
+                final int[] position = new int[2];
+                mThumbnailView.getLocationOnScreen(position);
+                final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
+                final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
+                final Rect taskBounds = new Rect(position[0], position[1],
+                        position[0] + width, position[1] + height);
+
+                // Take the thumbnail of the task without a scrim and apply it back after
+                float alpha = mThumbnailView.getDimAlpha();
+                mThumbnailView.setDimAlpha(0);
+                Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+                        taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
+                        Color.BLACK);
+                mThumbnailView.setDimAlpha(alpha);
+
+                AppTransitionAnimationSpecsFuture future =
+                        new AppTransitionAnimationSpecsFuture(mHandler) {
+                    @Override
+                    public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+                        return Collections.singletonList(new AppTransitionAnimationSpecCompat(
+                                taskId, thumbnail, taskBounds));
+                    }
+                };
+                WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
+                        future, animStartedListener, mHandler, true /* scaleUp */,
+                        taskKey.displayId);
+                mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
+                        .log(mLauncherEvent);
+            }
+        }
+    }
+
+    TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen,
+            R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) {
+
+        @Override
+        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+            // Don't show menu-item if already in multi-window and the task is from
+            // the secondary display.
+            // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
+            // implementation is enabled
+            return !activity.getDeviceProfile().isMultiWindowMode
+                    && (displayId == -1 || displayId == DEFAULT_DISPLAY);
+        }
+
+        @Override
+        protected ActivityOptions makeLaunchOptions(Activity activity) {
+            final ActivityCompat act = new ActivityCompat(activity);
+            final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
+                    act.getDisplayId());
+            if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
+                return null;
+            }
+            boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
+            return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
+        }
+
+        @Override
+        protected boolean onActivityStarted(BaseDraggingActivity activity) {
+            SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
+            activity.getUserEventDispatcher().logActionOnControl(TAP,
+                    LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
+            return true;
+        }
+    };
+
+    TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen,
+            R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) {
+
+        @Override
+        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+            return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
+        }
+
+        @Override
+        protected ActivityOptions makeLaunchOptions(Activity activity) {
+            ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
+            // Arbitrary bounds only because freeform is in dev mode right now
+            Rect r = new Rect(50, 50, 200, 200);
+            activityOptions.setLaunchBounds(r);
+            return activityOptions;
+        }
+
+        @Override
+        protected boolean onActivityStarted(BaseDraggingActivity activity) {
+            activity.returnToHomescreen();
+            return true;
+        }
+    };
+
+    TaskShortcutFactory PIN = (activity, tv) -> {
+        if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
+            return null;
+        }
+        if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
+            return null;
+        }
+        if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
+            // We shouldn't be able to pin while an app is locked.
+            return null;
+        }
+        return new PinSystemShortcut(activity, tv);
+    };
+
+    class PinSystemShortcut extends SystemShortcut {
+
+        private static final String TAG = "PinSystemShortcut";
+
+        private final TaskView mTaskView;
+
+        public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
+            super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo());
+            mTaskView = tv;
+        }
+
+        @Override
+        public void onClick(View view) {
+            Consumer<Boolean> resultCallback = success -> {
+                if (success) {
+                    SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(
+                            mTaskView.getTask().key.id);
+                } else {
+                    mTaskView.notifyTaskLaunchFailed(TAG);
+                }
+            };
+            mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler());
+            dismissTaskMenuView(mTarget);
+            mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
+                    .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
+        }
+    }
+
+    TaskShortcutFactory INSTALL = (activity, view) ->
+            InstantAppResolver.newInstance(activity).isInstantApp(activity,
+                 view.getTask().getTopComponent().getPackageName())
+                    ? new SystemShortcut.Install(activity, view.getItemInfo()) : null;
+
+    TaskShortcutFactory WELLBEING = (activity, view) ->
+            WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, view.getItemInfo());
+
+    TaskShortcutFactory SCREENSHOT = (activity, tv) -> {
+        if (ENABLE_OVERVIEW_ACTIONS.get()) {
+            return tv.getThumbnail().getTaskOverlay()
+                .getScreenshotShortcut(activity, tv.getItemInfo());
+        }
+        return null;
+    };
+
+    TaskShortcutFactory MODAL = (activity, tv) -> {
+        if (ENABLE_OVERVIEW_ACTIONS.get() && ENABLE_OVERVIEW_SELECTIONS.get()) {
+            return tv.getThumbnail().getTaskOverlay().getModalStateSystemShortcut(tv.getItemInfo());
+        }
+        return null;
+    };
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
deleted file mode 100644
index 51e869e..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
-
-import android.app.Activity;
-import android.app.ActivityOptions;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.InstantAppResolver;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
-import com.android.systemui.shared.recents.view.RecentsTransition;
-import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * Represents a system shortcut that can be shown for a recent task.
- */
-public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
-
-    private static final String TAG = "TaskSystemShortcut";
-
-    protected T mSystemShortcut;
-
-    public TaskSystemShortcut(T systemShortcut) {
-        super(systemShortcut);
-        mSystemShortcut = systemShortcut;
-    }
-
-    protected TaskSystemShortcut(int iconResId, int labelResId) {
-        super(iconResId, labelResId);
-    }
-
-    @Override
-    public View.OnClickListener getOnClickListener(
-            BaseDraggingActivity activity, ItemInfo itemInfo) {
-        return null;
-    }
-
-    public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
-        Task task = view.getTask();
-
-        WorkspaceItemInfo stubItemInfo = new WorkspaceItemInfo();
-        stubItemInfo.intent = new Intent();
-        ComponentName component = task.getTopComponent();
-        stubItemInfo.intent.setComponent(component);
-        stubItemInfo.user = UserHandle.of(task.key.userId);
-        stubItemInfo.title = TaskUtils.getTitle(activity, task);
-
-        return getOnClickListenerForTask(activity, task, stubItemInfo);
-    }
-
-    protected View.OnClickListener getOnClickListenerForTask(
-            BaseDraggingActivity activity, Task task, ItemInfo stubItemInfo) {
-        return mSystemShortcut.getOnClickListener(activity, stubItemInfo);
-    }
-
-    public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
-        public AppInfo() {
-            super(new SystemShortcut.AppInfo());
-        }
-    }
-
-    public static abstract class MultiWindow extends TaskSystemShortcut {
-
-        private Handler mHandler;
-
-        public MultiWindow(int iconRes, int textRes) {
-            super(iconRes, textRes);
-            mHandler = new Handler(Looper.getMainLooper());
-        }
-
-        protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
-        protected abstract ActivityOptions makeLaunchOptions(Activity activity);
-        protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
-
-        @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, TaskView taskView) {
-            final Task task  = taskView.getTask();
-            final int taskId = task.key.id;
-            final int displayId = task.key.displayId;
-            if (!task.isDockable) {
-                return null;
-            }
-            if (!isAvailable(activity, displayId)) {
-                return null;
-            }
-            final RecentsView recentsView = activity.getOverviewPanel();
-
-            final TaskThumbnailView thumbnailView = taskView.getThumbnail();
-            return (v -> {
-                final View.OnLayoutChangeListener onLayoutChangeListener =
-                        new View.OnLayoutChangeListener() {
-                            @Override
-                            public void onLayoutChange(View v, int l, int t, int r, int b,
-                                    int oldL, int oldT, int oldR, int oldB) {
-                                taskView.getRootView().removeOnLayoutChangeListener(this);
-                                recentsView.clearIgnoreResetTask(taskId);
-
-                                // Start animating in the side pages once launcher has been resized
-                                recentsView.dismissTask(taskView, false, false);
-                            }
-                        };
-
-                final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
-                        new DeviceProfile.OnDeviceProfileChangeListener() {
-                            @Override
-                            public void onDeviceProfileChanged(DeviceProfile dp) {
-                                activity.removeOnDeviceProfileChangeListener(this);
-                                if (dp.isMultiWindowMode) {
-                                    taskView.getRootView().addOnLayoutChangeListener(
-                                            onLayoutChangeListener);
-                                }
-                            }
-                        };
-
-                dismissTaskMenuView(activity);
-
-                ActivityOptions options = makeLaunchOptions(activity);
-                if (options != null
-                        && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
-                                options)) {
-                    if (!onActivityStarted(activity)) {
-                        return;
-                    }
-                    // Add a device profile change listener to kick off animating the side tasks
-                    // once we enter multiwindow mode and relayout
-                    activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
-
-                    final Runnable animStartedListener = () -> {
-                        // Hide the task view and wait for the window to be resized
-                        // TODO: Consider animating in launcher and do an in-place start activity
-                        //       afterwards
-                        recentsView.setIgnoreResetTask(taskId);
-                        taskView.setAlpha(0f);
-                    };
-
-                    final int[] position = new int[2];
-                    thumbnailView.getLocationOnScreen(position);
-                    final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX());
-                    final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY());
-                    final Rect taskBounds = new Rect(position[0], position[1],
-                            position[0] + width, position[1] + height);
-
-                    // Take the thumbnail of the task without a scrim and apply it back after
-                    float alpha = thumbnailView.getDimAlpha();
-                    thumbnailView.setDimAlpha(0);
-                    Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
-                            taskBounds.width(), taskBounds.height(), thumbnailView, 1f,
-                            Color.BLACK);
-                    thumbnailView.setDimAlpha(alpha);
-
-                    AppTransitionAnimationSpecsFuture future =
-                            new AppTransitionAnimationSpecsFuture(mHandler) {
-                        @Override
-                        public List<AppTransitionAnimationSpecCompat> composeSpecs() {
-                            return Collections.singletonList(new AppTransitionAnimationSpecCompat(
-                                    taskId, thumbnail, taskBounds));
-                        }
-                    };
-                    WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
-                            future, animStartedListener, mHandler, true /* scaleUp */, displayId);
-                }
-            });
-        }
-    }
-
-    public static class SplitScreen extends MultiWindow {
-        public SplitScreen() {
-            super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
-        }
-
-        @Override
-        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
-            // Don't show menu-item if already in multi-window and the task is from
-            // the secondary display.
-            // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
-            // implementation is enabled
-            return !activity.getDeviceProfile().isMultiWindowMode
-                    && (displayId == -1 || displayId == DEFAULT_DISPLAY);
-        }
-
-        @Override
-        protected ActivityOptions makeLaunchOptions(Activity activity) {
-            final ActivityCompat act = new ActivityCompat(activity);
-            final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
-                    act.getDisplayId());
-            if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
-                return null;
-            }
-            boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
-            return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
-        }
-
-        @Override
-        protected boolean onActivityStarted(BaseDraggingActivity activity) {
-            ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
-            try {
-                sysUiProxy.onSplitScreenInvoked();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to notify SysUI of split screen: ", e);
-                return false;
-            }
-            activity.getUserEventDispatcher().logActionOnControl(TAP,
-                    LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
-            return true;
-        }
-    }
-
-    public static class Freeform extends MultiWindow {
-        public Freeform() {
-            super(R.drawable.ic_split_screen, R.string.recent_task_option_freeform);
-        }
-
-        @Override
-        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
-            return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
-        }
-
-        @Override
-        protected ActivityOptions makeLaunchOptions(Activity activity) {
-            ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
-            // Arbitrary bounds only because freeform is in dev mode right now
-            Rect r = new Rect(50, 50, 200, 200);
-            activityOptions.setLaunchBounds(r);
-            return activityOptions;
-        }
-
-        @Override
-        protected boolean onActivityStarted(BaseDraggingActivity activity) {
-            activity.returnToHomescreen();
-            return true;
-        }
-    }
-
-    public static class Pin extends TaskSystemShortcut {
-
-        private static final String TAG = Pin.class.getSimpleName();
-
-        private Handler mHandler;
-
-        public Pin() {
-            super(R.drawable.ic_pin, R.string.recent_task_option_pin);
-            mHandler = new Handler(Looper.getMainLooper());
-        }
-
-        @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, TaskView taskView) {
-            ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
-            if (sysUiProxy == null) {
-                return null;
-            }
-            if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
-                return null;
-            }
-            if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
-                // We shouldn't be able to pin while an app is locked.
-                return null;
-            }
-            return view -> {
-                Consumer<Boolean> resultCallback = success -> {
-                    if (success) {
-                        try {
-                            sysUiProxy.startScreenPinning(taskView.getTask().key.id);
-                        } catch (RemoteException e) {
-                            Log.w(TAG, "Failed to start screen pinning: ", e);
-                        }
-                    } else {
-                        taskView.notifyTaskLaunchFailed(TAG);
-                    }
-                };
-                taskView.launchTask(true, resultCallback, mHandler);
-                dismissTaskMenuView(activity);
-            };
-        }
-    }
-
-    public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
-        public Install() {
-            super(new SystemShortcut.Install());
-        }
-
-        @Override
-        protected View.OnClickListener getOnClickListenerForTask(
-                BaseDraggingActivity activity, Task task, ItemInfo itemInfo) {
-            if (InstantAppResolver.newInstance(activity).isInstantApp(activity,
-                        task.getTopComponent().getPackageName())) {
-                return mSystemShortcut.createOnClickListener(activity, itemInfo);
-            }
-            return null;
-        }
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
index 6897c1e..e2e25f3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
@@ -15,33 +15,46 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 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.statehandlers.DepthController.DEPTH;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
 import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
 import android.graphics.RectF;
+import android.os.Build;
 import android.view.View;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Utilities;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.MultiValueUpdateListener;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 
 /**
  * Utility class for helpful methods related to {@link TaskView} objects and their tasks.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public final class TaskViewUtils {
 
     private TaskViewUtils() {}
@@ -107,70 +120,119 @@
     }
 
     /**
-     * @return Animator that controls the window of the opening targets for the recents launch
+     * Creates an animation that controls the window of the opening targets for the recents launch
      * animation.
      */
-    public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
-            RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) {
-        SyncRtSurfaceTransactionApplierCompat applier =
-                new SyncRtSurfaceTransactionApplierCompat(v);
-        ClipAnimationHelper.TransformParams params = new ClipAnimationHelper.TransformParams()
-                .setSyncTransactionApplier(applier);
+    public static void createRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets, DepthController depthController,
+            PendingAnimation out) {
 
-        final RemoteAnimationTargetSet targetSet =
-                new RemoteAnimationTargetSet(targets, MODE_OPENING);
-        targetSet.addDependentTransactionApplier(applier);
+        SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+        final RemoteAnimationTargets targets =
+                new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_OPENING);
+        targets.addReleaseCheck(applier);
+
+        TransformParams params = new TransformParams()
+                    .setSyncTransactionApplier(applier)
+                    .setTargetSet(targets);
 
         final RecentsView recentsView = v.getRecentsView();
-        final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
-        appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
-        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+        int taskIndex = recentsView.indexOfChild(v);
+        boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage();
+        int startScroll = recentsView.getScrollOffset(taskIndex);
 
-            // Defer fading out the view until after the app window gets faded in
-            final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR);
-            final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR);
+        Context context = v.getContext();
+        DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+        // RecentsView never updates the display rotation until swipe-up so the value may be stale.
+        // Use the display value instead.
+        int displayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
 
+        TaskViewSimulator topMostSimulator = null;
+        if (targets.apps.length > 0) {
+            TaskViewSimulator tsv = new TaskViewSimulator(context, recentsView.getSizeStrategy());
+            tsv.setDp(dp);
+            tsv.setLayoutRotation(displayRotation, displayRotation);
+            tsv.setPreview(targets.apps[targets.apps.length - 1]);
+            tsv.fullScreenProgress.value = 0;
+            tsv.recentsViewScale.value = 1;
+            tsv.setScroll(startScroll);
 
-            final RectF mThumbnailRect;
+            out.setFloat(tsv.fullScreenProgress,
+                    AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
+            out.setFloat(tsv.recentsViewScale,
+                    AnimatedFloat.VALUE, tsv.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+            out.setInt(tsv, TaskViewSimulator.SCROLL, 0, TOUCH_RESPONSE_INTERPOLATOR);
 
-            {
-                inOutHelper.setTaskAlphaCallback((t, alpha) -> mTaskAlpha.value);
+            out.addOnFrameCallback(() -> tsv.apply(params));
+            topMostSimulator = tsv;
+        }
 
-                inOutHelper.prepareAnimation(
-                        BaseActivity.fromContext(v.getContext()).getDeviceProfile(),
-                        true /* isOpening */);
-                inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(),
-                        targetSet.apps.length == 0 ? null : targetSet.apps[0]);
+        // Fade in the task during the initial 20% of the animation
+        out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1, clampToProgress(LINEAR, 0, 0.2f));
 
-                mThumbnailRect = new RectF(inOutHelper.getTargetRect());
-                mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY());
-                Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX());
-            }
+        if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulator != null) {
+            out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
 
-            @Override
-            public void onUpdate(float percent) {
-                // TODO: Take into account the current fullscreen progress for animating the insets
-                params.setProgress(1 - percent);
-                RectF taskBounds = inOutHelper.applyTransform(targetSet, params);
-                int taskIndex = recentsView.indexOfChild(v);
-                int centerTaskIndex = recentsView.getCurrentPage();
-                boolean parallaxCenterAndAdjacentTask = taskIndex != centerTaskIndex;
-                if (!skipViewChanges && parallaxCenterAndAdjacentTask) {
-                    float scale = taskBounds.width() / mThumbnailRect.width();
-                    v.setScaleX(scale);
-                    v.setScaleY(scale);
-                    v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX());
-                    v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY());
-                    v.setAlpha(mViewAlpha.value);
+            TaskViewSimulator simulatorToCopy = topMostSimulator;
+            simulatorToCopy.apply(params);
+
+            // Mt represents the overall transformation on the thumbnailView relative to the
+            // Launcher's rootView
+            // K(t) represents transformation on the running window by the taskViewSimulator at
+            // any time t.
+            // at t = 0, we know that the simulator matches the thumbnailView. So if we apply K(0)`
+            // on the Launcher's rootView, the thumbnailView would match the full running task
+            // window. If we apply "K(0)` K(t)" thumbnailView will match the final transformed
+            // window at any time t. This gives the overall matrix on thumbnailView to be:
+            //    Mt K(0)` K(t)
+            // During animation we apply transformation on the thumbnailView (and not the rootView)
+            // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
+            //    Mt K(0)` K(t) Mt`
+            TaskThumbnailView ttv = v.getThumbnail();
+            RectF tvBounds = new RectF(0, 0,  ttv.getWidth(), ttv.getHeight());
+            float[] tvBoundsMapped = new float[]{0, 0,  ttv.getWidth(), ttv.getHeight()};
+            getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false);
+            RectF tvBoundsInRoot = new RectF(
+                    tvBoundsMapped[0], tvBoundsMapped[1],
+                    tvBoundsMapped[2], tvBoundsMapped[3]);
+
+            Matrix mt = new Matrix();
+            mt.setRectToRect(tvBounds, tvBoundsInRoot, ScaleToFit.FILL);
+
+            Matrix mti = new Matrix();
+            mt.invert(mti);
+
+            Matrix k0i = new Matrix();
+            simulatorToCopy.getCurrentMatrix().invert(k0i);
+
+            Matrix animationMatrix = new Matrix();
+            out.addOnFrameCallback(() -> {
+                animationMatrix.set(mt);
+                animationMatrix.postConcat(k0i);
+                animationMatrix.postConcat(simulatorToCopy.getCurrentMatrix());
+                animationMatrix.postConcat(mti);
+                ttv.setAnimationMatrix(animationMatrix);
+            });
+
+            out.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    ttv.setAnimationMatrix(null);
                 }
-            }
-        });
-        appAnimator.addListener(new AnimatorListenerAdapter() {
+            });
+        }
+
+        out.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                targetSet.release();
+                targets.release();
             }
         });
-        return appAnimator;
+
+        if (depthController != null) {
+            out.setFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(context),
+                    TOUCH_RESPONSE_INTERPOLATOR);
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 2fa789f..6e0b517 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -15,94 +15,86 @@
  */
 package com.android.quickstep;
 
+import static android.content.Intent.ACTION_CHOOSER;
+import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
 
-import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
-import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_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.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
-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_HOME_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
-import android.app.ActivityManager.RunningTaskInfo;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
 import android.app.Service;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.content.res.Configuration;
-import android.graphics.Point;
-import android.graphics.RectF;
+import android.graphics.Rect;
 import android.graphics.Region;
+import android.graphics.drawable.Icon;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Process;
-import android.os.RemoteException;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputEvent;
 import android.view.MotionEvent;
-import android.view.Surface;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.BaseFlags;
-import com.android.launcher3.logging.EventLogArray;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.launcher3.tracing.nano.LauncherTraceProto;
+import com.android.launcher3.tracing.nano.TouchInteractionServiceProto;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
-import com.android.quickstep.inputconsumers.AssistantTouchConsumer;
+import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
-import com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer;
-import com.android.quickstep.inputconsumers.InputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
+import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.ProtoTracer;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.systemui.plugins.OverscrollPlugin;
+import com.android.systemui.plugins.PluginListener;
 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.InputEventReceiver;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
-import com.android.systemui.shared.system.RecentsAnimationListener;
-import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
+import com.android.systemui.shared.tracing.ProtoTraceable;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -130,52 +122,55 @@
 /**
  * Service connected by system-UI for handling touch interaction.
  */
-@TargetApi(Build.VERSION_CODES.Q)
-public class TouchInteractionService extends Service implements
-        NavigationModeChangeListener, DefaultDisplay.DisplayInfoChangeListener {
-
-    /**
-     * NOTE: This value should be kept same as
-     * ActivityTaskManagerService#INTENT_EXTRA_LOG_TRACE_ID in platform
-     */
-    public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID";
-
-
-    public static final EventLogArray TOUCH_INTERACTION_LOG =
-            new EventLogArray("touch_interaction_log", 40);
+@TargetApi(Build.VERSION_CODES.R)
+public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin>,
+        ProtoTraceable<LauncherTraceProto> {
 
     private static final String TAG = "TouchInteractionService";
 
     private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
     private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
+    private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
     private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
+
+    /**
+     * System Action ID to show all apps.
+     * TODO: Use AccessibilityService's corresponding global action constant in S
+     */
+    private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
+
     private int mBackGestureNotificationCounter = -1;
+    @Nullable
+    private OverscrollPlugin mOverscrollPlugin;
 
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
-        public void onActiveNavBarRegionChanges(Region region) {
-            mActiveNavBarRegion = region;
-        }
-
+        @BinderThread
         public void onInitialize(Bundle bundle) {
-            mISystemUiProxy = ISystemUiProxy.Stub
-                    .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            MAIN_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
-            MAIN_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet);
-            MAIN_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */));
+            ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
+            MAIN_EXECUTOR.execute(() -> {
+                SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy);
+                TouchInteractionService.this.initInputMonitor();
+                preloadOverview(true /* fromInit */);
+            });
             sIsInitialized = true;
         }
 
+        @BinderThread
         @Override
         public void onOverviewToggle() {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
             mOverviewCommandHelper.onOverviewToggle();
         }
 
+        @BinderThread
         @Override
         public void onOverviewShown(boolean triggeredFromAltTab) {
             mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
         }
 
+        @BinderThread
         @Override
         public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
             if (triggeredFromAltTab && !triggeredFromHomeKey) {
@@ -184,42 +179,63 @@
             }
         }
 
+        @BinderThread
         @Override
         public void onTip(int actionType, int viewType) {
             mOverviewCommandHelper.onTip(actionType, viewType);
         }
 
+        @BinderThread
         @Override
         public void onAssistantAvailable(boolean available) {
-            mAssistantAvailable = available;
+            MAIN_EXECUTOR.execute(() -> {
+                mDeviceState.setAssistantAvailable(available);
+                TouchInteractionService.this.onAssistantVisibilityChanged();
+            });
         }
 
+        @BinderThread
         @Override
         public void onAssistantVisibilityChanged(float visibility) {
-            mLastAssistantVisibility = visibility;
-            MAIN_EXECUTOR.execute(
-                    TouchInteractionService.this::onAssistantVisibilityChanged);
+            MAIN_EXECUTOR.execute(() -> {
+                mDeviceState.setAssistantVisibility(visibility);
+                TouchInteractionService.this.onAssistantVisibilityChanged();
+            });
         }
 
+        @BinderThread
         public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
                 boolean gestureSwipeLeft) {
             if (mOverviewComponentObserver == null) {
                 return;
             }
 
-            final ActivityControlHelper activityControl =
-                    mOverviewComponentObserver.getActivityControlHelper();
+            final BaseActivityInterface activityInterface =
+                    mOverviewComponentObserver.getActivityInterface();
             UserEventDispatcher.newInstance(getBaseContext()).logActionBack(completed, downX, downY,
-                    isButton, gestureSwipeLeft, activityControl.getContainerType());
+                    isButton, gestureSwipeLeft, activityInterface.getContainerType());
 
             if (completed && !isButton && shouldNotifyBackGesture()) {
                 UI_HELPER_EXECUTOR.execute(TouchInteractionService.this::tryNotifyBackGesture);
             }
         }
 
+        @BinderThread
         public void onSystemUiStateChanged(int stateFlags) {
-            mSystemUiStateFlags = stateFlags;
-            MAIN_EXECUTOR.execute(TouchInteractionService.this::onSystemUiFlagsChanged);
+            MAIN_EXECUTOR.execute(() -> {
+                mDeviceState.setSystemUiFlags(stateFlags);
+                TouchInteractionService.this.onSystemUiFlagsChanged();
+            });
+        }
+
+        @BinderThread
+        public void onActiveNavBarRegionChanges(Region region) {
+            MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region));
+        }
+
+        public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets)  {
+            WindowBounds wb = new WindowBounds(bounds, insets);
+            MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
         }
 
         /** Deprecated methods **/
@@ -242,8 +258,6 @@
 
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
-    private static final SwipeSharedState sSwipeSharedState = new SwipeSharedState();
-    private int mLogId;
 
     public static boolean isConnected() {
         return sConnected;
@@ -253,91 +267,39 @@
         return sIsInitialized;
     }
 
-    public static SwipeSharedState getSwipeSharedState() {
-        return sSwipeSharedState;
-    }
-
-    private final InputConsumer mResetGestureInputConsumer =
-            new ResetGestureInputConsumer(sSwipeSharedState);
-
-    private final BaseSwipeUpHandler.Factory mWindowTreansformFactory =
-            this::createWindowTransformSwipeHandler;
-    private final BaseSwipeUpHandler.Factory mFallbackNoButtonFactory =
-            this::createFallbackNoButtonSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+            this::createLauncherSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+            this::createFallbackSwipeHandler;
 
     private ActivityManagerWrapper mAM;
-    private RecentsModel mRecentsModel;
-    private ISystemUiProxy mISystemUiProxy;
     private OverviewCommandHelper mOverviewCommandHelper;
     private OverviewComponentObserver mOverviewComponentObserver;
-    private OverviewInteractionState mOverviewInteractionState;
-    private OverviewCallbacks mOverviewCallbacks;
-    private TaskOverlayFactory mTaskOverlayFactory;
     private InputConsumerController mInputConsumer;
-    private boolean mAssistantAvailable;
-    private float mLastAssistantVisibility = 0;
-    private @SystemUiStateFlags int mSystemUiStateFlags;
-
-    private boolean mIsUserUnlocked;
-    private BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
-                initWhenUserUnlocked();
-            }
-        }
-    };
+    private RecentsAnimationDeviceState mDeviceState;
+    private TaskAnimationManager mTaskAnimationManager;
 
     private InputConsumer mUncheckedConsumer = InputConsumer.NO_OP;
     private InputConsumer mConsumer = InputConsumer.NO_OP;
     private Choreographer mMainChoreographer;
-
-    private Region mActiveNavBarRegion = new Region();
+    private InputConsumer mResetGestureInputConsumer;
+    private GestureState mGestureState = DEFAULT_STATE;
 
     private InputMonitorCompat mInputMonitorCompat;
     private InputEventReceiver mInputEventReceiver;
-    private Mode mMode = Mode.THREE_BUTTONS;
-    private int mDefaultDisplayId;
-    private final RectF mSwipeTouchRegion = new RectF();
-    private final RectF mAssistantLeftRegion = new RectF();
-    private final RectF mAssistantRightRegion = new RectF();
-
-    private ComponentName mGestureBlockingActivity;
-
-    private Region mExclusionRegion;
-    private SystemGestureExclusionListenerCompat mExclusionListener;
 
     @Override
     public void onCreate() {
         super.onCreate();
-
         // Initialize anything here that is needed in direct boot mode.
-        // Everything else should be initialized in initWhenUserUnlocked() below.
+        // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
         mAM = ActivityManagerWrapper.getInstance();
+        mDeviceState = new RecentsAnimationDeviceState(this);
+        mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+        mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
+        ProtoTracer.INSTANCE.get(this).add(this);
 
-        if (UserManagerCompat.getInstance(this).isUserUnlocked(Process.myUserHandle())) {
-            initWhenUserUnlocked();
-        } else {
-            mIsUserUnlocked = false;
-            registerReceiver(mUserUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
-        }
-
-        mDefaultDisplayId = DefaultDisplay.INSTANCE.get(this).getInfo().id;
-        String blockingActivity = getString(R.string.gesture_blocking_activity);
-        mGestureBlockingActivity = TextUtils.isEmpty(blockingActivity) ? null :
-                ComponentName.unflattenFromString(blockingActivity);
-
-        mExclusionListener = new SystemGestureExclusionListenerCompat(mDefaultDisplayId) {
-            @Override
-            @BinderThread
-            public void onExclusionChanged(Region region) {
-                // Assignments are atomic, it should be safe on binder thread
-                mExclusionRegion = region;
-            }
-        };
-
-        onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this));
         sConnected = true;
     }
 
@@ -345,9 +307,6 @@
         if (mInputEventReceiver != null) {
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "disposeEventHandlers");
-            }
         }
         if (mInputMonitorCompat != null) {
             mInputMonitorCompat.dispose();
@@ -356,126 +315,37 @@
     }
 
     private void initInputMonitor() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
-        }
-        if (!mMode.hasGestures || mISystemUiProxy == null) {
-            return;
-        }
         disposeEventHandlers();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 2");
-        }
-
-        try {
-            mInputMonitorCompat = InputMonitorCompat.fromBundle(mISystemUiProxy
-                    .monitorGestureInput("swipe-up", mDefaultDisplayId), KEY_EXTRA_INPUT_MONITOR);
-            mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
-                    mMainChoreographer, this::onInputEvent);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 3");
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Unable to create input monitor", e);
-        }
-        initTouchBounds();
-    }
-
-    private int getNavbarSize(String resName) {
-        return ResourceUtils.getNavbarSize(resName, getResources());
-    }
-
-    private void initTouchBounds() {
-        if (!mMode.hasGestures) {
+        if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
             return;
         }
 
-        DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(this).getInfo();
-        Point realSize = new Point(displayInfo.realSize);
-        mSwipeTouchRegion.set(0, 0, realSize.x, realSize.y);
-        if (mMode == Mode.NO_BUTTON) {
-            int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
-            mSwipeTouchRegion.top = mSwipeTouchRegion.bottom - touchHeight;
+        Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
+                mDeviceState.getDisplayId());
+        mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
+        mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
+                mMainChoreographer, this::onInputEvent);
 
-            final int assistantWidth = getResources()
-                    .getDimensionPixelSize(R.dimen.gestures_assistant_width);
-            final float assistantHeight = Math.max(touchHeight,
-                    QuickStepContract.getWindowCornerRadius(getResources()));
-            mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mSwipeTouchRegion.bottom;
-            mAssistantLeftRegion.top = mAssistantRightRegion.top =
-                    mSwipeTouchRegion.bottom - assistantHeight;
-
-            mAssistantLeftRegion.left = 0;
-            mAssistantLeftRegion.right = assistantWidth;
-
-            mAssistantRightRegion.right = mSwipeTouchRegion.right;
-            mAssistantRightRegion.left = mSwipeTouchRegion.right - assistantWidth;
-        } else {
-            mAssistantLeftRegion.setEmpty();
-            mAssistantRightRegion.setEmpty();
-            switch (displayInfo.rotation) {
-                case Surface.ROTATION_90:
-                    mSwipeTouchRegion.left = mSwipeTouchRegion.right
-                            - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
-                    break;
-                case Surface.ROTATION_270:
-                    mSwipeTouchRegion.right = mSwipeTouchRegion.left
-                            + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
-                    break;
-                default:
-                    mSwipeTouchRegion.top = mSwipeTouchRegion.bottom
-                            - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
-            }
-        }
+        mDeviceState.updateGestureTouchRegions();
     }
 
-    @Override
-    public void onNavigationModeChanged(Mode newMode) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
-        }
-        if (mMode.hasGestures != newMode.hasGestures) {
-            if (newMode.hasGestures) {
-                DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
-            } else {
-                DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
-            }
-        }
-        mMode = newMode;
-
-        disposeEventHandlers();
+    /**
+     * Called when the navigation mode changes, guaranteed to be after the device state has updated.
+     */
+    private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
         initInputMonitor();
-
-        if (mMode == Mode.NO_BUTTON) {
-            mExclusionListener.register();
-        } else {
-            mExclusionListener.unregister();
-        }
+        resetHomeBounceSeenOnQuickstepEnabledFirstTime();
     }
 
-    @Override
-    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
-        if (info.id != mDefaultDisplayId) {
-            return;
-        }
-
-        initTouchBounds();
-    }
-
-    private void initWhenUserUnlocked() {
-        mRecentsModel = RecentsModel.INSTANCE.get(this);
-        mOverviewComponentObserver = new OverviewComponentObserver(this);
-
-        mOverviewCommandHelper = new OverviewCommandHelper(this, mOverviewComponentObserver);
-        mOverviewInteractionState = OverviewInteractionState.INSTANCE.get(this);
-        mOverviewCallbacks = OverviewCallbacks.get(this);
-        mTaskOverlayFactory = TaskOverlayFactory.INSTANCE.get(this);
+    @UiThread
+    public void onUserUnlocked() {
+        mTaskAnimationManager = new TaskAnimationManager();
+        mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
+        mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
+                mOverviewComponentObserver);
+        mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager);
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
-        mIsUserUnlocked = true;
-
-        sSwipeSharedState.setOverviewComponentObserver(mOverviewComponentObserver);
         mInputConsumer.registerInputConsumer();
-        onSystemUiProxySet();
         onSystemUiFlagsChanged();
         onAssistantVisibilityChanged();
 
@@ -483,51 +353,92 @@
         // new ModelPreload().start(this);
         mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
                 .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
+        resetHomeBounceSeenOnQuickstepEnabledFirstTime();
 
-        Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
+        PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
+                OverscrollPlugin.class, false /* allowMultiple */);
+
+        mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
+        onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
     }
 
-    @UiThread
-    private void onSystemUiProxySet() {
-        if (mIsUserUnlocked) {
-            mRecentsModel.setSystemUiProxy(mISystemUiProxy);
-            mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy);
+    private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
+        if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
+            // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
+            // mode doesn't have gestures
+            return;
+        }
+
+        // Reset home bounce seen on quick step enabled for first time
+        SharedPreferences sharedPrefs = Utilities.getPrefs(this);
+        if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) {
+            sharedPrefs.edit()
+                    .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
+                    .putBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN, false)
+                    .apply();
+        }
+    }
+
+    private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
+        AccessibilityManager am = getSystemService(AccessibilityManager.class);
+
+        if (isHomeAndOverviewSame) {
+            Intent intent = new Intent(mOverviewComponentObserver.getHomeIntent())
+                    .setAction(Intent.ACTION_ALL_APPS);
+            RemoteAction allAppsAction = new RemoteAction(
+                    Icon.createWithResource(this, R.drawable.ic_apps),
+                    getString(R.string.all_apps_label),
+                    getString(R.string.all_apps_label),
+                    PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent,
+                            PendingIntent.FLAG_UPDATE_CURRENT));
+            am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
+        } else {
+            am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
         }
     }
 
     @UiThread
     private void onSystemUiFlagsChanged() {
-        if (mIsUserUnlocked) {
-            mOverviewInteractionState.setSystemUiStateFlags(mSystemUiStateFlags);
-            mOverviewComponentObserver.onSystemUiStateChanged(mSystemUiStateFlags);
+        if (mDeviceState.isUserUnlocked()) {
+            SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(
+                    mDeviceState.getSystemUiStateFlags());
+            mOverviewComponentObserver.onSystemUiStateChanged();
+
+            // Update the tracing state
+            if ((mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_TRACING_ENABLED) != 0) {
+                ProtoTracer.INSTANCE.get(TouchInteractionService.this).start();
+            } else {
+                ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
+            }
         }
     }
 
     @UiThread
     private void onAssistantVisibilityChanged() {
-        if (mIsUserUnlocked) {
-            mOverviewComponentObserver.getActivityControlHelper().onAssistantVisibilityChanged(
-                    mLastAssistantVisibility);
+        if (mDeviceState.isUserUnlocked()) {
+            mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
+                    mDeviceState.getAssistantVisibility());
         }
     }
 
     @Override
     public void onDestroy() {
         sIsInitialized = false;
-        if (mIsUserUnlocked) {
+        if (mDeviceState.isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
             mOverviewComponentObserver.onDestroy();
+            PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
         }
         disposeEventHandlers();
-        if (mMode.hasGestures) {
-            DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
-        }
+        mDeviceState.destroy();
+        SystemUiProxy.INSTANCE.get(this).setProxy(null);
+        ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
+        ProtoTracer.INSTANCE.get(this).remove(this);
+
+        getSystemService(AccessibilityManager.class)
+                .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
 
         sConnected = false;
-        Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
-        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
-        mExclusionListener.unregister();
-
         super.onDestroy();
     }
 
@@ -538,249 +449,334 @@
     }
 
     private void onInputEvent(InputEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onInputEvent " + ev);
-        }
         if (!(ev instanceof MotionEvent)) {
             Log.e(TAG, "Unknown event " + ev);
             return;
         }
-
         MotionEvent event = (MotionEvent) ev;
-        if (event.getAction() == ACTION_DOWN) {
-            mLogId = TOUCH_INTERACTION_LOG.generateAndSetLogId();
-            sSwipeSharedState.setLogTraceId(mLogId);
 
-            if (mSwipeTouchRegion.contains(event.getX(), event.getY())) {
-                boolean useSharedState = mConsumer.useSharedSwipeState();
+        TestLogging.recordMotionEvent(
+                TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
+
+        if (!mDeviceState.isUserUnlocked()) {
+            return;
+        }
+
+        Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
+                TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
+
+        final int action = event.getAction();
+        if (action == ACTION_DOWN) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
+            }
+            mDeviceState.setOrientationTransformIfNeeded(event);
+
+            if (mDeviceState.isInSwipeUpTouchRegion(event)) {
+                if (TestProtocol.sDebugTracing) {
+                    Log.d(TestProtocol.NO_SWIPE_TO_HOME,
+                            "TouchInteractionService.onInputEvent:isInSwipeUpTouchRegion");
+                }
+                // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
+                // onConsumerInactive and wipe the previous gesture state
+                GestureState prevGestureState = new GestureState(mGestureState);
+                GestureState newGestureState = createGestureState(mGestureState);
                 mConsumer.onConsumerAboutToBeSwitched();
-                mConsumer = newConsumer(useSharedState, event);
-                TOUCH_INTERACTION_LOG.addLog("setInputConsumer", mConsumer.getType());
+                mGestureState = newGestureState;
+                mConsumer = newConsumer(prevGestureState, mGestureState, event);
+
+                ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName());
                 mUncheckedConsumer = mConsumer;
-            } else if (mIsUserUnlocked && mMode == Mode.NO_BUTTON
-                    && canTriggerAssistantAction(event)) {
-                // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we should
-                // not interrupt it. QuickSwitch assumes that interruption can only happen if the
-                // next gesture is also quick switch.
-                mUncheckedConsumer =
-                        new AssistantTouchConsumer(
-                                this,
-                                mISystemUiProxy,
-                                mOverviewComponentObserver.getActivityControlHelper(),
-                                InputConsumer.NO_OP,
-                                mInputMonitorCompat,
-                                mOverviewComponentObserver.assistantGestureIsConstrained());
+            } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()) {
+                mGestureState = createGestureState(mGestureState);
+                ActivityManager.RunningTaskInfo runningTask = mGestureState.getRunningTask();
+                if (mDeviceState.canTriggerAssistantAction(event, runningTask)) {
+                    // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
+                    // should not interrupt it. QuickSwitch assumes that interruption can only
+                    // happen if the next gesture is also quick switch.
+                    mUncheckedConsumer = new AssistantInputConsumer(
+                            this,
+                            mGestureState,
+                            InputConsumer.NO_OP, mInputMonitorCompat,
+                            mOverviewComponentObserver.assistantGestureIsConstrained());
+                } else {
+                    mUncheckedConsumer = InputConsumer.NO_OP;
+                }
             } else {
                 mUncheckedConsumer = InputConsumer.NO_OP;
             }
+        } else {
+            // Other events
+            if (mUncheckedConsumer != InputConsumer.NO_OP) {
+                // Only transform the event if we are handling it in a proper consumer
+                mDeviceState.setOrientationTransformIfNeeded(event);
+            }
         }
 
-        TOUCH_INTERACTION_LOG.addLog("onMotionEvent", event.getActionMasked());
+        if (mUncheckedConsumer != InputConsumer.NO_OP) {
+            switch (event.getActionMasked()) {
+                case ACTION_DOWN:
+                case ACTION_UP:
+                    ActiveGestureLog.INSTANCE.addLog("onMotionEvent("
+                            + (int) event.getRawX() + ", " + (int) event.getRawY() + ")",
+                            event.getActionMasked());
+                    break;
+                default:
+                    ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+                    break;
+            }
+        }
+
+        boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
+                && mConsumer != null
+                && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
         mUncheckedConsumer.onMotionEvent(event);
+
+        if (cleanUpConsumer) {
+            reset();
+        }
+        TraceHelper.INSTANCE.endFlagsOverride(traceToken);
     }
 
-    private boolean validSystemUiFlags() {
-        return (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
-                && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
-                        || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
+    private GestureState createGestureState(GestureState previousGestureState) {
+        GestureState gestureState = new GestureState(mOverviewComponentObserver,
+                ActiveGestureLog.INSTANCE.generateAndSetLogId());
+        if (mTaskAnimationManager.isRecentsAnimationRunning()) {
+            gestureState.updateRunningTask(previousGestureState.getRunningTask());
+            gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
+            gestureState.updatePreviouslyAppearedTaskIds(
+                    previousGestureState.getPreviouslyAppearedTaskIds());
+        } else {
+            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
+                    () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
+        }
+        return gestureState;
     }
 
-    private boolean canTriggerAssistantAction(MotionEvent ev) {
-        return mAssistantAvailable
-                && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
-                && (mAssistantLeftRegion.contains(ev.getX(), ev.getY()) ||
-                    mAssistantRightRegion.contains(ev.getX(), ev.getY()))
-                && !ActivityManagerWrapper.getInstance().isLockToAppActive();
-    }
+    private InputConsumer newConsumer(GestureState previousGestureState,
+            GestureState newGestureState, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer");
+        }
+        boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
 
-    private InputConsumer newConsumer(boolean useSharedState, MotionEvent event) {
-        boolean isInValidSystemUiState = validSystemUiFlags();
-
-        if (!mIsUserUnlocked) {
-            if (isInValidSystemUiState) {
+        if (!mDeviceState.isUserUnlocked()) {
+            if (canStartSystemGesture) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
-                return createDeviceLockedInputConsumer(mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
+                return createDeviceLockedInputConsumer(newGestureState);
             } else {
                 return mResetGestureInputConsumer;
             }
         }
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer:user is unlocked");
+        }
 
-        // When using sharedState, bypass systemState check as this is a followup gesture and the
-        // first gesture started in a valid system state.
-        InputConsumer base = isInValidSystemUiState || useSharedState
-                ? newBaseConsumer(useSharedState, event) : mResetGestureInputConsumer;
-        if (mMode == Mode.NO_BUTTON) {
-            final ActivityControlHelper activityControl =
-                    mOverviewComponentObserver.getActivityControlHelper();
-            if (canTriggerAssistantAction(event)) {
-                base = new AssistantTouchConsumer(
-                        this,
-                        mISystemUiProxy,
-                        activityControl,
-                        base,
-                        mInputMonitorCompat,
-                        mOverviewComponentObserver.assistantGestureIsConstrained());
+        // When there is an existing recents animation running, bypass systemState check as this is
+        // a followup gesture and the first gesture started in a valid system state.
+        InputConsumer base = canStartSystemGesture
+                || previousGestureState.isRecentsAnimationRunning()
+                        ? newBaseConsumer(previousGestureState, newGestureState, event)
+                        : mResetGestureInputConsumer;
+        if (mDeviceState.isGesturalNavMode()) {
+            handleOrientationSetup(base);
+        }
+        if (mDeviceState.isFullyGesturalNavMode()) {
+            if (mDeviceState.canTriggerAssistantAction(event, newGestureState.getRunningTask())) {
+                base = new AssistantInputConsumer(
+                    this,
+                    newGestureState,
+                    base,
+                    mInputMonitorCompat,
+                    mOverviewComponentObserver.assistantGestureIsConstrained());
             }
 
-            if ((mSystemUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0) {
+            if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
+                OverscrollPlugin plugin = null;
+                if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
+                    TaskOverlayFactory factory =
+                            TaskOverlayFactory.INSTANCE.get(getApplicationContext());
+                    plugin = factory.getLocalOverscrollPlugin();  // may be null
+                }
+
+                // If not local plugin was forced, use the actual overscroll plugin if available.
+                if (plugin == null && mOverscrollPlugin != null && mOverscrollPlugin.isActive()) {
+                    plugin = mOverscrollPlugin;
+                }
+
+                if (plugin != null) {
+                    // Put the overscroll gesture as higher priority than the Assistant or base
+                    // gestures
+                    base = new OverscrollInputConsumer(this, newGestureState, base,
+                        mInputMonitorCompat, plugin);
+                }
+            }
+
+            // If Bubbles is expanded, use the overlay input consumer, which will close Bubbles
+            // instead of going all the way home when a swipe up is detected.
+            if (mDeviceState.isBubblesExpanded() || mDeviceState.isGlobalActionsShowing()) {
+                base = new SysUiOverlayInputConsumer(
+                        getBaseContext(), mDeviceState, mInputMonitorCompat);
+            }
+
+            if (mDeviceState.isScreenPinningActive()) {
                 // Note: we only allow accessibility to wrap this, and it replaces the previous
                 // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
-                base = new ScreenPinnedInputConsumer(this, mISystemUiProxy, activityControl);
+                base = new ScreenPinnedInputConsumer(this, newGestureState);
             }
 
-            if ((mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0) {
-                base = new AccessibilityInputConsumer(this, mISystemUiProxy,
-                        (mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0, base,
-                        mInputMonitorCompat, mSwipeTouchRegion);
+            if (mDeviceState.isAccessibilityMenuAvailable()) {
+                base = new AccessibilityInputConsumer(this, mDeviceState, base,
+                        mInputMonitorCompat);
             }
         } else {
-            if ((mSystemUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0) {
+            if (mDeviceState.isScreenPinningActive()) {
                 base = mResetGestureInputConsumer;
             }
         }
         return base;
     }
 
-    private InputConsumer newBaseConsumer(boolean useSharedState, MotionEvent event) {
-        RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
-        if (!useSharedState) {
-            sSwipeSharedState.clearAllState(false /* finishAnimation */);
+    private void handleOrientationSetup(InputConsumer baseInputConsumer) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "handleOrientationSetup.1");
         }
-        if ((mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0) {
+
+        baseInputConsumer.notifyOrientationSetup();
+    }
+
+    private InputConsumer newBaseConsumer(GestureState previousGestureState,
+            GestureState gestureState, MotionEvent event) {
+        if (mDeviceState.isKeyguardShowingOccluded()) {
             // This handles apps showing over the lockscreen (e.g. camera)
-            return createDeviceLockedInputConsumer(runningTaskInfo);
+            return createDeviceLockedInputConsumer(gestureState);
         }
 
-        final ActivityControlHelper activityControl =
-                mOverviewComponentObserver.getActivityControlHelper();
-
-        boolean forceOverviewInputConsumer = false;
-        if (AssistantUtilities.isExcludedAssistant(runningTaskInfo)) {
+        // Use overview input consumer for sharesheets on top of home.
+        boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted()
+                && gestureState.getRunningTask() != null
+                && ACTION_CHOOSER.equals(gestureState.getRunningTask().baseIntent.getAction());
+        if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-            runningTaskInfo = mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT /* ignoreActivityType */);
+            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
+                    () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
             ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
-            ComponentName runningComponent = runningTaskInfo.baseIntent.getComponent();
+            ComponentName runningComponent =
+                    gestureState.getRunningTask().baseIntent.getComponent();
             forceOverviewInputConsumer =
-                runningComponent != null && runningComponent.equals(homeComponent);
+                    runningComponent != null && runningComponent.equals(homeComponent);
         }
 
-        if (runningTaskInfo == null && !sSwipeSharedState.goingToLauncher
-                && !sSwipeSharedState.recentsAnimationFinishInterrupted) {
+        if (gestureState.getRunningTask() == null) {
             return mResetGestureInputConsumer;
-        } else if (sSwipeSharedState.recentsAnimationFinishInterrupted) {
-            // If the finish animation was interrupted, then continue using the other activity input
-            // consumer but with the next task as the running task
-            RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
-            info.id = sSwipeSharedState.nextRunningTaskId;
-            return createOtherActivityInputConsumer(event, info);
-        } else if (sSwipeSharedState.goingToLauncher || activityControl.isResumed()
+        } else if (previousGestureState.isRunningAnimationToLauncher()
+                || gestureState.getActivityInterface().isResumed()
                 || forceOverviewInputConsumer) {
-            return createOverviewInputConsumer(event, forceOverviewInputConsumer);
-        } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityControl.isInLiveTileMode()) {
-            return createOverviewInputConsumer(event, forceOverviewInputConsumer);
-        } else if (mGestureBlockingActivity != null && runningTaskInfo != null
-                && mGestureBlockingActivity.equals(runningTaskInfo.topActivity)) {
+            return createOverviewInputConsumer(
+                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
+        } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()
+                && gestureState.getActivityInterface().isInLiveTileMode()) {
+            return createOverviewInputConsumer(
+                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
+        } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
             return mResetGestureInputConsumer;
         } else {
-            return createOtherActivityInputConsumer(event, runningTaskInfo);
+            return createOtherActivityInputConsumer(gestureState, event);
         }
     }
 
-    private boolean disableHorizontalSwipe(MotionEvent event) {
-        // mExclusionRegion can change on binder thread, use a local instance here.
-        Region exclusionRegion = mExclusionRegion;
-        return mMode == Mode.NO_BUTTON && exclusionRegion != null
-                && exclusionRegion.contains((int) event.getX(), (int) event.getY());
-    }
+    private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
+            MotionEvent event) {
 
-    private InputConsumer createOtherActivityInputConsumer(MotionEvent event,
-            RunningTaskInfo runningTaskInfo) {
-
-        final boolean shouldDefer;
         final BaseSwipeUpHandler.Factory factory;
-
-        if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            shouldDefer = !sSwipeSharedState.recentsAnimationFinishInterrupted;
-            factory = mFallbackNoButtonFactory;
+        if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
+            factory = mFallbackSwipeHandlerFactory;
         } else {
-            shouldDefer = mOverviewComponentObserver.getActivityControlHelper()
-                    .deferStartingActivity(mActiveNavBarRegion, event);
-            factory = mWindowTreansformFactory;
+            factory = mLauncherSwipeHandlerFactory;
         }
 
-        return new OtherActivityInputConsumer(this, runningTaskInfo,
-                shouldDefer, mOverviewCallbacks, this::onConsumerInactive,
-                sSwipeSharedState, mInputMonitorCompat, mSwipeTouchRegion,
-                disableHorizontalSwipe(event), factory, mLogId);
+        final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame()
+                || gestureState.getActivityInterface().deferStartingActivity(mDeviceState, event);
+        final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
+        return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
+                gestureState, shouldDefer, this::onConsumerInactive,
+                mInputMonitorCompat, disableHorizontalSwipe, factory);
     }
 
-    private InputConsumer createDeviceLockedInputConsumer(RunningTaskInfo taskInfo) {
-        if (mMode == Mode.NO_BUTTON && taskInfo != null) {
-            return new DeviceLockedInputConsumer(this, sSwipeSharedState, mInputMonitorCompat,
-                    mSwipeTouchRegion, taskInfo.taskId, mLogId);
+    private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
+        if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
+            return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
+                    gestureState, mInputMonitorCompat);
         } else {
             return mResetGestureInputConsumer;
         }
     }
 
-    public InputConsumer createOverviewInputConsumer(MotionEvent event,
+    public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
+            GestureState gestureState, MotionEvent event,
             boolean forceOverviewInputConsumer) {
-        final ActivityControlHelper activityControl =
-                mOverviewComponentObserver.getActivityControlHelper();
-        BaseDraggingActivity activity = activityControl.getCreatedActivity();
+        StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity();
         if (activity == null) {
             return mResetGestureInputConsumer;
         }
 
         if (activity.getRootView().hasWindowFocus()
-                || sSwipeSharedState.goingToLauncher
-                || (BaseFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+                || previousGestureState.isRunningAnimationToLauncher()
+                || (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
                     && forceOverviewInputConsumer)) {
-            return new OverviewInputConsumer(activity, mInputMonitorCompat,
+            return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
-            return new OverviewWithoutFocusInputConsumer(activity, mInputMonitorCompat,
-                    disableHorizontalSwipe(event));
+            final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
+            return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState,
+                    mInputMonitorCompat, disableHorizontalSwipe);
         }
     }
 
     /**
-     * To be called by the consumer when it's no longer active.
+     * To be called by the consumer when it's no longer active. This can be called by any consumer
+     * in the hierarchy at any point during the gesture (ie. if a delegate consumer starts
+     * intercepting touches, the base consumer can try to call this).
      */
     private void onConsumerInactive(InputConsumer caller) {
-        if (mConsumer == caller) {
-            mConsumer = mResetGestureInputConsumer;
-            mUncheckedConsumer = mConsumer;
+        if (mConsumer != null && mConsumer.getActiveConsumerInHierarchy() == caller) {
+            reset();
         }
     }
 
+    private void reset() {
+        mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
+        mGestureState = DEFAULT_STATE;
+    }
+
     private void preloadOverview(boolean fromInit) {
-        if (!mIsUserUnlocked) {
+        if (!mDeviceState.isUserUnlocked()) {
             return;
         }
-        if (!mMode.hasGestures && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+
+        if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
             // Prevent the overview from being started before the real home on first boot.
             return;
         }
 
-        if (RestoreDbTask.isPending(this)) {
+        if (RestoreDbTask.isPending(this) || !mDeviceState.isUserSetupComplete()) {
             // Preloading while a restore is pending may cause launcher to start the restore
             // too early.
             return;
         }
 
-        final ActivityControlHelper<BaseDraggingActivity> activityControl =
-                mOverviewComponentObserver.getActivityControlHelper();
-        if (activityControl.getCreatedActivity() == null) {
+        final BaseActivityInterface activityInterface =
+                mOverviewComponentObserver.getActivityInterface();
+        final Intent overviewIntent = new Intent(
+                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
+        if (activityInterface.getCreatedActivity() == null) {
             // Make sure that UI states will be initialized.
-            activityControl.createActivityInitListener((activity, wasVisible) -> {
-                AppLaunchTracker.INSTANCE.get(activity);
+            activityInterface.createActivityInitListener((wasVisible) -> {
+                AppLaunchTracker.INSTANCE.get(TouchInteractionService.this);
                 return false;
-            }).register();
+            }).register(overviewIntent);
         } else if (fromInit) {
             // The activity has been created before the initialization of overview service. It is
             // usually happens when booting or launcher is the top activity, so we should already
@@ -788,19 +784,17 @@
             return;
         }
 
-        // Pass null animation handler to indicate this start is preload.
-        startRecentsActivityAsync(mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(),
-                null);
+        mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        if (!mIsUserUnlocked) {
+        if (!mDeviceState.isUserUnlocked()) {
             return;
         }
-        final ActivityControlHelper activityControl =
-                mOverviewComponentObserver.getActivityControlHelper();
-        final BaseDraggingActivity activity = activityControl.getCreatedActivity();
+        final BaseActivityInterface activityInterface =
+                mOverviewComponentObserver.getActivityInterface();
+        final BaseDraggingActivity activity = activityInterface.getCreatedActivity();
         if (activity == null || activity.isStarted()) {
             // We only care about the existing background activity.
             return;
@@ -828,32 +822,30 @@
             }
         } else {
             // Dump everything
-            pw.println("TouchState:");
-            pw.println("  navMode=" + mMode);
-            pw.println("  validSystemUiFlags=" + validSystemUiFlags());
-            pw.println("  systemUiFlags=" + mSystemUiStateFlags);
-            pw.println("  systemUiFlagsDesc="
-                    + QuickStepContract.getSystemUiStateString(mSystemUiStateFlags));
-            pw.println("  assistantAvailable=" + mAssistantAvailable);
-            pw.println("  assistantDisabled="
-                    + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-            boolean resumed = mOverviewComponentObserver != null
-                    && mOverviewComponentObserver.getActivityControlHelper().isResumed();
-            pw.println("  resumed=" + resumed);
-            pw.println("  useSharedState=" + mConsumer.useSharedSwipeState());
-            if (mConsumer.useSharedSwipeState()) {
-                sSwipeSharedState.dump("    ", pw);
+            FeatureFlags.dump(pw);
+            if (mDeviceState.isUserUnlocked()) {
+                PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw);
             }
+            mDeviceState.dump(pw);
+            if (mOverviewComponentObserver != null) {
+                mOverviewComponentObserver.dump(pw);
+            }
+            if (mGestureState != null) {
+                mGestureState.dump(pw);
+            }
+            SysUINavigationMode.INSTANCE.get(this).dump(pw);
+            pw.println("TouchState:");
+            BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
+                    : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
+            boolean resumed = mOverviewComponentObserver != null
+                    && mOverviewComponentObserver.getActivityInterface().isResumed();
+            pw.println("  createdOverviewActivity=" + createdOverviewActivity);
+            pw.println("  resumed=" + resumed);
             pw.println("  mConsumer=" + mConsumer.getName());
-            pw.println("FeatureFlags:");
-            pw.println("  APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
-            pw.println("  QUICKSTEP_SPRINGS=" + QUICKSTEP_SPRINGS.get());
-            pw.println("  ADAPTIVE_ICON_WINDOW_ANIM=" + ADAPTIVE_ICON_WINDOW_ANIM.get());
-            pw.println("  ENABLE_QUICKSTEP_LIVE_TILE=" + ENABLE_QUICKSTEP_LIVE_TILE.get());
-            pw.println("  ENABLE_HINTS_IN_OVERVIEW=" + ENABLE_HINTS_IN_OVERVIEW.get());
-            pw.println("  FAKE_LANDSCAPE_UI=" + FAKE_LANDSCAPE_UI.get());
-            TOUCH_INTERACTION_LOG.dump("", pw);
-
+            ActiveGestureLog.INSTANCE.dump("", pw);
+            pw.println("ProtoTrace:");
+            pw.println("  file="
+                    + ProtoTracer.INSTANCE.get(TouchInteractionService.this).getTraceFile());
         }
     }
 
@@ -865,26 +857,26 @@
     private void onCommand(PrintWriter pw, ArgList args) {
         switch (args.nextArg()) {
             case "clear-touch-log":
-                TOUCH_INTERACTION_LOG.clear();
+                ActiveGestureLog.INSTANCE.clear();
                 break;
         }
     }
 
-    private BaseSwipeUpHandler createWindowTransformSwipeHandler(RunningTaskInfo runningTask,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return  new WindowTransformSwipeHandler(runningTask, this, touchTimeMs,
-                mOverviewComponentObserver, continuingLastGesture, mInputConsumer, mRecentsModel);
+    private BaseSwipeUpHandler createLauncherSwipeHandler(
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+        return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
+                gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(RunningTaskInfo runningTask,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new FallbackNoButtonInputConsumer(this, mOverviewComponentObserver, runningTask,
-                mRecentsModel, mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
+    private BaseSwipeUpHandler createFallbackSwipeHandler(
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+        return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+                gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
     protected boolean shouldNotifyBackGesture() {
         return mBackGestureNotificationCounter > 0 &&
-                mGestureBlockingActivity != null;
+                !mDeviceState.getGestureBlockedActivityPackages().isEmpty();
     }
 
     @WorkerThread
@@ -893,13 +885,27 @@
             mBackGestureNotificationCounter--;
             Utilities.getDevicePrefs(this).edit()
                     .putInt(KEY_BACK_NOTIFICATION_COUNT, mBackGestureNotificationCounter).apply();
-            sendBroadcast(new Intent(NOTIFY_ACTION_BACK).setPackage(
-                    mGestureBlockingActivity.getPackageName()));
+            mDeviceState.getGestureBlockedActivityPackages().forEach(blockedPackage ->
+                    sendBroadcast(new Intent(NOTIFY_ACTION_BACK).setPackage(blockedPackage)));
         }
     }
 
-    public static void startRecentsActivityAsync(Intent intent, RecentsAnimationListener listener) {
-        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(intent, null, listener, null, null));
+    @Override
+    public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
+        mOverscrollPlugin = overscrollPlugin;
+    }
+
+    @Override
+    public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
+        mOverscrollPlugin = null;
+    }
+
+    @Override
+    public void writeToProto(LauncherTraceProto proto) {
+        if (proto.touchInteractionService == null) {
+            proto.touchInteractionService = new TouchInteractionServiceProto();
+        }
+        proto.touchInteractionService.serviceConnected = true;
+        proto.touchInteractionService.serviceConnected = true;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java
new file mode 100644
index 0000000..cbb6ad4
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.graphics.Canvas;
+import android.view.View;
+
+import com.android.systemui.shared.system.WindowCallbacksCompat;
+
+import java.util.function.BooleanSupplier;
+
+/**
+ * Utility class for helpful methods related to {@link View} objects.
+ */
+public class ViewUtils {
+
+    /** See {@link #postDraw(View, Runnable, BooleanSupplier)}} */
+    public static boolean postDraw(View view, Runnable onFinishRunnable) {
+        return postDraw(view, onFinishRunnable, () -> false);
+    }
+
+    /**
+     * Inject some addition logic in order to make sure that the view is updated smoothly post
+     * draw, and allow addition task to be run after view update.
+     *
+     * @param onFinishRunnable runnable to be run right after the view finishes drawing.
+     */
+    public static boolean postDraw(View view, Runnable onFinishRunnable, BooleanSupplier canceled) {
+        // Defer finishing the animation until the next launcher frame with the
+        // new thumbnail
+        return new WindowCallbacksCompat(view) {
+            // The number of frames to defer until we actually finish the animation
+            private int mDeferFrameCount = 2;
+
+            @Override
+            public void onPostDraw(Canvas canvas) {
+                // If we were cancelled after this was attached, do not update
+                // the state.
+                if (canceled.getAsBoolean()) {
+                    detach();
+                    return;
+                }
+
+                if (mDeferFrameCount > 0) {
+                    mDeferFrameCount--;
+                    // Workaround, detach and reattach to invalidate the root node for
+                    // another draw
+                    detach();
+                    attach();
+                    view.invalidate();
+                    return;
+                }
+
+                if (onFinishRunnable != null) {
+                    onFinishRunnable.run();
+                }
+                detach();
+            }
+        }.attach();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
deleted file mode 100644
index 2fa4feb..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ /dev/null
@@ -1,1233 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
-import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.launcher3.util.RaceConditionTracker.ENTER;
-import static com.android.launcher3.util.RaceConditionTracker.EXIT;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
-import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
-import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
-import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Canvas;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.View;
-import android.view.View.OnApplyWindowInsetsListener;
-import android.view.ViewTreeObserver.OnDrawListener;
-import android.view.WindowInsets;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.RaceConditionTracker;
-import com.android.launcher3.util.TraceHelper;
-import com.android.quickstep.ActivityControlHelper.AnimationFactory;
-import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.inputconsumers.InputConsumer;
-import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.util.ClipAnimationHelper.TargetAlphaProvider;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
-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;
-import com.android.systemui.shared.system.InputConsumerController;
-import com.android.systemui.shared.system.LatencyTrackerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.WindowCallbacksCompat;
-
-@TargetApi(Build.VERSION_CODES.O)
-public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
-        extends BaseSwipeUpHandler<T, RecentsView>
-        implements OnApplyWindowInsetsListener {
-    private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
-
-    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
-
-    private static int getFlagForIndex(int index, String name) {
-        if (DEBUG_STATES) {
-            STATE_NAMES[index] = name;
-        }
-        return 1 << index;
-    }
-
-    // Launcher UI related states
-    private static final int STATE_LAUNCHER_PRESENT = getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
-    private static final int STATE_LAUNCHER_STARTED = getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
-    private static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
-
-    // Internal initialization states
-    private static final int STATE_APP_CONTROLLER_RECEIVED =
-            getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED");
-
-    // Interaction finish states
-    private static final int STATE_SCALED_CONTROLLER_HOME =
-            getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
-    private static final int STATE_SCALED_CONTROLLER_RECENTS =
-            getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
-
-    private static final int STATE_HANDLER_INVALIDATED =
-            getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
-    private static final int STATE_GESTURE_STARTED =
-            getFlagForIndex(7, "STATE_GESTURE_STARTED");
-    private static final int STATE_GESTURE_CANCELLED =
-            getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
-    private static final int STATE_GESTURE_COMPLETED =
-            getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
-
-    private static final int STATE_CAPTURE_SCREENSHOT =
-            getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
-    private static final int STATE_SCREENSHOT_CAPTURED =
-            getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
-    private static final int STATE_SCREENSHOT_VIEW_SHOWN =
-            getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
-
-    private static final int STATE_RESUME_LAST_TASK =
-            getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
-    private static final int STATE_START_NEW_TASK =
-            getFlagForIndex(14, "STATE_START_NEW_TASK");
-    private static final int STATE_CURRENT_TASK_FINISHED =
-            getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
-
-    private static final int LAUNCHER_UI_STATES =
-            STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
-
-    public enum GestureEndTarget {
-        HOME(1, STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT, true, false,
-                ContainerType.WORKSPACE, false),
-
-        RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true),
-
-        NEW_TASK(0, STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT, false, true,
-                ContainerType.APP, true),
-
-        LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false);
-
-        GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued,
-                int containerType, boolean recentsAttachedToAppWindow) {
-            this.endShift = endShift;
-            this.endState = endState;
-            this.isLauncher = isLauncher;
-            this.canBeContinued = canBeContinued;
-            this.containerType = containerType;
-            this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
-        }
-
-        /** 0 is app, 1 is overview */
-        public final float endShift;
-        /** The state to apply when we reach this final target */
-        public final int endState;
-        /** Whether the target is in the launcher activity */
-        public final boolean isLauncher;
-        /** Whether the user can start a new gesture while this one is finishing */
-        public final boolean canBeContinued;
-        /** Used to log where the user ended up after the gesture ends */
-        public final int containerType;
-        /** Whether RecentsView should be attached to the window as we animate to this target */
-        public final boolean recentsAttachedToAppWindow;
-    }
-
-    public static final long MAX_SWIPE_DURATION = 350;
-    public static final long MIN_SWIPE_DURATION = 80;
-    public static final long MIN_OVERSHOOT_DURATION = 120;
-
-    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
-    private static final float SWIPE_DURATION_MULTIPLIER =
-            Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
-    private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
-
-    public static final long RECENTS_ATTACH_DURATION = 300;
-
-    /**
-     * Used as the page index for logging when we return to the last task at the end of the gesture.
-     */
-    private static final int LOG_NO_OP_PAGE_INDEX = -1;
-
-    private GestureEndTarget mGestureEndTarget;
-    // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
-    private RunningWindowAnim mRunningWindowAnim;
-    private boolean mIsShelfPeeking;
-
-    private boolean mContinuingLastGesture;
-
-    private ThumbnailData mTaskSnapshot;
-
-    // Used to control launcher components throughout the swipe gesture.
-    private AnimatorPlaybackController mLauncherTransitionController;
-    private boolean mHasLauncherTransitionControllerStarted;
-
-    private AnimationFactory mAnimationFactory = (t) -> { };
-    private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay();
-    private boolean mLiveTileOverlayAttached = false;
-
-    private boolean mWasLauncherAlreadyVisible;
-
-    private boolean mPassedOverviewThreshold;
-    private boolean mGestureStarted;
-    private int mLogAction = Touch.SWIPE;
-    private int mLogDirection = Direction.UP;
-    private PointF mDownPos;
-    private boolean mIsLikelyToStartNewTask;
-
-    private final long mTouchTimeMs;
-    private long mLauncherFrameDrawnTime;
-
-    public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context,
-            long touchTimeMs, OverviewComponentObserver overviewComponentObserver,
-            boolean continuingLastGesture,
-            InputConsumerController inputConsumer, RecentsModel recentsModel) {
-        super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
-        mTouchTimeMs = touchTimeMs;
-        mContinuingLastGesture = continuingLastGesture;
-        initStateCallbacks();
-    }
-
-    private void initStateCallbacks() {
-        mStateCallback = new MultiStateCallback(STATE_NAMES);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
-                this::onLauncherPresentAndGestureStarted);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
-                this::initializeLauncherAnimationController);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
-                this::launcherFrameDrawn);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
-                        | STATE_GESTURE_CANCELLED,
-                this::resetStateForAnimationCancel);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
-                this::sendRemoteAnimationsToAnimationFactory);
-
-        mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
-                this::resumeLastTask);
-        mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
-                this::startNewTask);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
-                        | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
-                this::switchToScreenshot);
-
-        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
-                        | STATE_SCALED_CONTROLLER_RECENTS,
-                this::finishCurrentTransitionToRecents);
-
-        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
-                        | STATE_SCALED_CONTROLLER_HOME,
-                this::finishCurrentTransitionToHome);
-        mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
-                this::reset);
-
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
-                        | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
-                        | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
-                        | STATE_GESTURE_STARTED,
-                this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
-
-        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
-                this::invalidateHandlerWithLauncher);
-        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
-                this::notifyTransitionCancelled);
-
-        mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
-                mRecentsAnimationWrapper::enableInputConsumer);
-
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
-                            | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
-                    (b) -> mRecentsView.setRunningTaskHidden(!b));
-        }
-    }
-
-    @Override
-    protected boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
-        if (mActivity == activity) {
-            return true;
-        }
-        if (mActivity != null) {
-            // The launcher may have been recreated as a result of device rotation.
-            int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
-            initStateCallbacks();
-            mStateCallback.setState(oldState);
-        }
-        mWasLauncherAlreadyVisible = alreadyOnHome;
-        mActivity = activity;
-        // Override the visibility of the activity until the gesture actually starts and we swipe
-        // up, or until we transition home and the home animation is composed
-        if (alreadyOnHome) {
-            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
-        } else {
-            mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
-        }
-
-        mRecentsView = activity.getOverviewPanel();
-        linkRecentsViewScroll();
-        addLiveTileOverlay();
-
-        mStateCallback.setState(STATE_LAUNCHER_PRESENT);
-        if (alreadyOnHome) {
-            onLauncherStart(activity);
-        } else {
-            activity.setOnStartCallback(this::onLauncherStart);
-        }
-
-        setupRecentsViewUi();
-        return true;
-    }
-
-    @Override
-    protected boolean moveWindowWithRecentsScroll() {
-        return mGestureEndTarget != HOME;
-    }
-
-    private void onLauncherStart(final T activity) {
-        if (mActivity != activity) {
-            return;
-        }
-        if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
-            return;
-        }
-
-        // If we've already ended the gesture and are going home, don't prepare recents UI,
-        // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
-        if (mGestureEndTarget != HOME) {
-            Runnable initAnimFactory = () -> {
-                mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
-                        mWasLauncherAlreadyVisible, true,
-                        this::onAnimatorPlaybackControllerCreated);
-                maybeUpdateRecentsAttachedState(false /* animate */);
-            };
-            if (mWasLauncherAlreadyVisible) {
-                // Launcher is visible, but might be about to stop. Thus, if we prepare recents
-                // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
-                // wait until the next gesture (and possibly launcher) starts.
-                mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
-            } else {
-                initAnimFactory.run();
-            }
-        }
-        AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
-                AbstractFloatingView.TYPE_LISTENER);
-
-        if (mWasLauncherAlreadyVisible) {
-            mStateCallback.setState(STATE_LAUNCHER_DRAWN);
-        } else {
-            TraceHelper.beginSection("WTS-init");
-            View dragLayer = activity.getDragLayer();
-            dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
-
-                @Override
-                public void onDraw() {
-                    TraceHelper.endSection("WTS-init", "Launcher frame is drawn");
-                    dragLayer.post(() ->
-                            dragLayer.getViewTreeObserver().removeOnDrawListener(this));
-                    if (activity != mActivity) {
-                        return;
-                    }
-
-                    mStateCallback.setState(STATE_LAUNCHER_DRAWN);
-                }
-            });
-        }
-
-        activity.getRootView().setOnApplyWindowInsetsListener(this);
-        mStateCallback.setState(STATE_LAUNCHER_STARTED);
-    }
-
-    private void onLauncherPresentAndGestureStarted() {
-        // Re-setup the recents UI when gesture starts, as the state could have been changed during
-        // that time by a previous window transition.
-        setupRecentsViewUi();
-
-        notifyGestureStartedAsync();
-    }
-
-    private void setupRecentsViewUi() {
-        if (mContinuingLastGesture) {
-            updateSysUiFlags(mCurrentShift.value);
-            return;
-        }
-        mRecentsView.onGestureAnimationStart(mRunningTaskId);
-    }
-
-    private void launcherFrameDrawn() {
-        mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
-    }
-
-    private void sendRemoteAnimationsToAnimationFactory() {
-        mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet);
-    }
-
-    private void initializeLauncherAnimationController() {
-        buildAnimationController();
-
-        if (LatencyTrackerCompat.isEnabled(mContext)) {
-            LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs));
-        }
-
-        // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
-        // high-res thumbnail loader here once we are sure that we will end up in an overview state
-        RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
-                .getHighResLoadingState().setVisible(true);
-    }
-
-    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
-        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 +
-                mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
-        return TaskView.getCurveScaleForInterpolation(interpolation);
-    }
-
-    @Override
-    public void onMotionPauseChanged(boolean isPaused) {
-        setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
-    }
-
-    public void maybeUpdateRecentsAttachedState() {
-        maybeUpdateRecentsAttachedState(true /* animate */);
-    }
-
-    /**
-     * Determines whether to show or hide RecentsView. The window is always
-     * synchronized with its corresponding TaskView in RecentsView, so if
-     * RecentsView is shown, it will appear to be attached to the window.
-     *
-     * Note this method has no effect unless the navigation mode is NO_BUTTON.
-     */
-    private void maybeUpdateRecentsAttachedState(boolean animate) {
-        if (mMode != Mode.NO_BUTTON || mRecentsView == null) {
-            return;
-        }
-        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationWrapper.targetSet == null
-                ? null
-                : mRecentsAnimationWrapper.targetSet.findTask(mRunningTaskId);
-        final boolean recentsAttachedToAppWindow;
-        int runningTaskIndex = mRecentsView.getRunningTaskIndex();
-        if (mGestureEndTarget != null) {
-            recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow;
-        } else if (mContinuingLastGesture
-                && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
-            recentsAttachedToAppWindow = true;
-            animate = false;
-        } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
-            // The window is going away so make sure recents is always visible in this case.
-            recentsAttachedToAppWindow = true;
-            animate = false;
-        } else {
-            recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
-            if (animate) {
-                // Only animate if an adjacent task view is visible on screen.
-                TaskView adjacentTask1 = mRecentsView.getNextTaskView();
-                TaskView adjacentTask2 = mRecentsView.getPreviousTaskView();
-                float prevTranslationX = mRecentsView.getTranslationX();
-                mRecentsView.setTranslationX(0);
-                animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
-                        || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT));
-                mRecentsView.setTranslationX(prevTranslationX);
-            }
-        }
-        mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
-    }
-
-    @Override
-    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
-        if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
-            mIsLikelyToStartNewTask = isLikelyToStartNewTask;
-            maybeUpdateRecentsAttachedState();
-        }
-    }
-
-    @UiThread
-    public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
-        mAnimationFactory.setShelfState(shelfState, interpolator, duration);
-        boolean wasShelfPeeking = mIsShelfPeeking;
-        mIsShelfPeeking = shelfState == PEEK;
-        if (mIsShelfPeeking != wasShelfPeeking) {
-            maybeUpdateRecentsAttachedState();
-        }
-        if (shelfState.shouldPreformHaptic) {
-            performHapticFeedback();
-        }
-    }
-
-    private void buildAnimationController() {
-        if (mGestureEndTarget == HOME || mHasLauncherTransitionControllerStarted) {
-            // We don't want a new mLauncherTransitionController if mGestureEndTarget == HOME (it
-            // has its own animation) or if we're already animating the current controller.
-            return;
-        }
-        initTransitionEndpoints(mActivity.getDeviceProfile());
-        mAnimationFactory.createActivityController(mTransitionDragLength);
-    }
-
-    @Override
-    public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
-        WindowInsets result = view.onApplyWindowInsets(windowInsets);
-        buildAnimationController();
-        return result;
-    }
-
-    private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
-        mLauncherTransitionController = anim;
-        mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mAnimationFactory.adjustActivityControllerInterpolators();
-        mLauncherTransitionController.dispatchOnStart();
-        updateLauncherTransitionProgress();
-    }
-
-    @Override
-    public Intent getLaunchIntent() {
-        return mOverviewComponentObserver.getOverviewIntent();
-    }
-
-    @Override
-    public void updateFinalShift() {
-
-        SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
-        if (controller != null) {
-            applyTransformUnchecked();
-            updateSysUiFlags(mCurrentShift.value);
-        }
-
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (mRecentsAnimationWrapper.getController() != null) {
-                mLiveTileOverlay.update(mClipAnimationHelper.getCurrentRectWithInsets(),
-                        mClipAnimationHelper.getCurrentCornerRadius());
-            }
-        }
-
-        final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
-        if (passed != mPassedOverviewThreshold) {
-            mPassedOverviewThreshold = passed;
-            if (mMode != Mode.NO_BUTTON) {
-                performHapticFeedback();
-            }
-        }
-
-        if (mLauncherTransitionController == null || mLauncherTransitionController
-                .getAnimationPlayer().isStarted()) {
-            return;
-        }
-        updateLauncherTransitionProgress();
-    }
-
-    private void updateLauncherTransitionProgress() {
-        if (mGestureEndTarget == HOME) {
-            return;
-        }
-        // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
-        // anyway. The controller mimics the drag length factor by applying it to its interpolators.
-        float progress = mCurrentShift.value / mDragLengthFactor;
-        mLauncherTransitionController.setPlayFraction(progress);
-    }
-
-    /**
-     * @param windowProgress 0 == app, 1 == overview
-     */
-    private void updateSysUiFlags(float windowProgress) {
-        if (mRecentsView != null) {
-            TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
-            int centermostTaskFlags = centermostTask == null ? 0
-                    : centermostTask.getThumbnail().getSysUiStatusNavFlags();
-            boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
-            // We will handle the sysui flags based on the centermost task view.
-            mRecentsAnimationWrapper.setWindowThresholdCrossed(centermostTaskFlags != 0
-                    || useHomeScreenFlags);
-            int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags;
-            mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        super.onRecentsAnimationStart(targetSet);
-        TOUCH_INTERACTION_LOG.addLog("startRecentsAnimationCallback", targetSet.apps.length);
-        setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
-
-        mPassedOverviewThreshold = false;
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled() {
-        mRecentsAnimationWrapper.setController(null);
-        mActivityInitListener.unregister();
-        setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
-        TOUCH_INTERACTION_LOG.addLog("cancelRecentsAnimation");
-    }
-
-    @Override
-    public void onGestureStarted() {
-        notifyGestureStartedAsync();
-        setStateOnUiThread(STATE_GESTURE_STARTED);
-        mGestureStarted = true;
-    }
-
-    /**
-     * Notifies the launcher that the swipe gesture has started. This can be called multiple times.
-     */
-    @UiThread
-    private void notifyGestureStartedAsync() {
-        final T curActivity = mActivity;
-        if (curActivity != null) {
-            // Once the gesture starts, we can no longer transition home through the button, so
-            // reset the force override of the activity visibility
-            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
-        }
-    }
-
-    /**
-     * Called as a result on ACTION_CANCEL to return the UI to the start state.
-     */
-    @Override
-    public void onGestureCancelled() {
-        updateDisplacement(0);
-        setStateOnUiThread(STATE_GESTURE_COMPLETED);
-        mLogAction = Touch.SWIPE_NOOP;
-        handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
-    }
-
-    /**
-     * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
-     * @param velocity The x and y components of the velocity when the gesture ends.
-     * @param downPos The x and y value of where the gesture started.
-     */
-    @Override
-    public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
-        float flingThreshold = mContext.getResources()
-                .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-        boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
-        setStateOnUiThread(STATE_GESTURE_COMPLETED);
-
-        mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
-        boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
-        if (isVelocityVertical) {
-            mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN;
-        } else {
-            mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
-        }
-        mDownPos = downPos;
-        handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
-    }
-
-    @Override
-    protected InputConsumer createNewInputProxyHandler() {
-        endRunningWindowAnim(mGestureEndTarget == HOME /* cancel */);
-        endLauncherTransitionController();
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            // Hide the task view, if not already hidden
-            setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
-        }
-
-        BaseDraggingActivity activity = mActivityControlHelper.getCreatedActivity();
-        return activity == null
-                ? InputConsumer.NO_OP : new OverviewInputConsumer(activity, null, true);
-    }
-
-    private void endRunningWindowAnim(boolean cancel) {
-        if (mRunningWindowAnim != null) {
-            if (cancel) {
-                mRunningWindowAnim.cancel();
-            } else {
-                mRunningWindowAnim.end();
-            }
-        }
-    }
-
-    private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
-            boolean isCancel) {
-        final GestureEndTarget endTarget;
-        final boolean goingToNewTask;
-        if (mRecentsView != null) {
-            if (!mRecentsAnimationWrapper.hasTargets()) {
-                // If there are no running tasks, then we can assume that this is a continuation of
-                // the last gesture, but after the recents animation has finished
-                goingToNewTask = true;
-            } else {
-                final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
-                final int taskToLaunch = mRecentsView.getNextPage();
-                goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
-            }
-        } else {
-            goingToNewTask = false;
-        }
-        final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
-        if (!isFling) {
-            if (isCancel) {
-                endTarget = LAST_TASK;
-            } else if (mMode == Mode.NO_BUTTON) {
-                if (mIsShelfPeeking) {
-                    endTarget = RECENTS;
-                } else if (goingToNewTask) {
-                    endTarget = NEW_TASK;
-                } else {
-                    endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME;
-                }
-            } else {
-                endTarget = reachedOverviewThreshold && mGestureStarted
-                        ? RECENTS
-                        : goingToNewTask
-                                ? NEW_TASK
-                                : LAST_TASK;
-            }
-        } else {
-            // If swiping at a diagonal, base end target on the faster velocity.
-            boolean isSwipeUp = endVelocity < 0;
-            boolean willGoToNewTaskOnSwipeUp =
-                    goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
-
-            if (mMode == Mode.NO_BUTTON && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
-                endTarget = HOME;
-            } else if (mMode == Mode.NO_BUTTON && isSwipeUp && !mIsShelfPeeking) {
-                // If swiping at a diagonal, base end target on the faster velocity.
-                endTarget = NEW_TASK;
-            } else if (isSwipeUp) {
-                endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp
-                        ? NEW_TASK : RECENTS;
-            } else {
-                endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
-            }
-        }
-
-        int stateFlags = OverviewInteractionState.INSTANCE.get(mActivity).getSystemUiStateFlags();
-        if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0
-                && (endTarget == RECENTS || endTarget == LAST_TASK)) {
-            return LAST_TASK;
-        }
-        return endTarget;
-    }
-
-    @UiThread
-    private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
-            boolean isCancel) {
-        PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
-        long duration = MAX_SWIPE_DURATION;
-        float currentShift = mCurrentShift.value;
-        final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
-                isFling, isCancel);
-        float endShift = endTarget.endShift;
-        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 - velocityPxPerMs.y
-                    * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
-            float minFlingVelocity = mContext.getResources()
-                    .getDimension(R.dimen.quickstep_fling_min_velocity);
-            if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
-                if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) {
-                    Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
-                            startShift, endShift, endShift, endVelocity / 1000,
-                            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
-                    // which the user flings, so we scale the duration by a value near to the
-                    // derivative of the scroll interpolator at zero, ie. 2.
-                    long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
-                    duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-
-                    if (endTarget == RECENTS) {
-                        interpolator = OVERSHOOT_1_2;
-                    }
-                }
-            }
-        }
-
-        if (endTarget.isLauncher) {
-            mRecentsAnimationWrapper.enableInputProxy();
-        }
-
-        if (endTarget == HOME) {
-            setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
-            duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
-        } else if (endTarget == RECENTS) {
-            mLiveTileOverlay.startIconAnimation();
-            if (mRecentsView != null) {
-                int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
-                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.
-                    mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
-                }
-                if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
-                    mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
-                }
-                duration = Math.max(duration, mRecentsView.getScroller().getDuration());
-            }
-            if (mMode == Mode.NO_BUTTON) {
-                setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
-            }
-        } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) {
-            // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
-            // or resumeLastTask().
-            if (mRecentsView != null) {
-                duration = Math.max(duration, mRecentsView.getScroller().getDuration());
-            }
-        }
-        animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
-    }
-
-    private void doLogGesture(GestureEndTarget endTarget) {
-        DeviceProfile dp = mDp;
-        if (dp == null || mDownPos == null) {
-            // We probably never received an animation controller, skip logging.
-            return;
-        }
-
-        int pageIndex = endTarget == LAST_TASK
-                ? LOG_NO_OP_PAGE_INDEX
-                : mRecentsView.getNextPage();
-        UserEventDispatcher.newInstance(mContext).logStateChangeAction(
-                mLogAction, mLogDirection,
-                (int) mDownPos.x, (int) mDownPos.y,
-                ContainerType.NAVBAR, ContainerType.APP,
-                endTarget.containerType,
-                pageIndex);
-    }
-
-    /** Animates to the given progress, where 0 is the current app and 1 is overview. */
-    @UiThread
-    private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
-            GestureEndTarget target, PointF velocityPxPerMs) {
-        mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
-                interpolator, target, velocityPxPerMs));
-    }
-
-    @UiThread
-    private void animateToProgressInternal(float start, float end, long duration,
-            Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
-        mGestureEndTarget = target;
-
-        maybeUpdateRecentsAttachedState();
-
-        if (mGestureEndTarget == HOME) {
-            HomeAnimationFactory homeAnimFactory;
-            if (mActivity != null) {
-                homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
-            } else {
-                homeAnimFactory = new HomeAnimationFactory() {
-                    @NonNull
-                    @Override
-                    public RectF getWindowTargetRect() {
-                        RectF fallbackTarget = new RectF(mClipAnimationHelper.getTargetRect());
-                        Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f);
-                        return fallbackTarget;
-                    }
-
-                    @NonNull
-                    @Override
-                    public AnimatorPlaybackController createActivityAnimationToHome() {
-                        return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
-                    }
-                };
-                mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
-                        isPresent -> mRecentsView.startHome());
-            }
-            RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
-            windowAnim.addAnimatorListener(new AnimationSuccessListener() {
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    setStateOnUiThread(target.endState);
-                }
-            });
-            windowAnim.start(velocityPxPerMs);
-            homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
-            mLauncherTransitionController = null;
-        } else {
-            ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
-            windowAnim.setDuration(duration).setInterpolator(interpolator);
-            windowAnim.addUpdateListener(valueAnimator -> {
-                if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
-                    // Views typically don't compute scroll when invisible as an optimization,
-                    // but in our case we need to since the window offset depends on the scroll.
-                    mRecentsView.computeScroll();
-                }
-            });
-            windowAnim.addListener(new AnimationSuccessListener() {
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    if (target == NEW_TASK && mRecentsView != null
-                            && mRecentsView.getNextPage() == mRecentsView.getRunningTaskIndex()) {
-                        // We are about to launch the current running task, so use LAST_TASK state
-                        // instead of NEW_TASK. This could happen, for example, if our scroll is
-                        // aborted after we determined the target to be NEW_TASK.
-                        setStateOnUiThread(LAST_TASK.endState);
-                    } else {
-                        setStateOnUiThread(target.endState);
-                    }
-                }
-            });
-            windowAnim.start();
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
-        }
-        // Always play the entire launcher animation when going home, since it is separate from
-        // the animation that has been controlled thus far.
-        if (mGestureEndTarget == HOME) {
-            start = 0;
-        }
-
-        // We want to use the same interpolator as the window, but need to adjust it to
-        // interpolate over the remaining progress (end - start).
-        TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
-                interpolator, start, end);
-        if (mLauncherTransitionController == null) {
-            return;
-        }
-        if (start == end || duration <= 0) {
-            mLauncherTransitionController.dispatchSetInterpolator(t -> end);
-        } else {
-            mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-            mAnimationFactory.adjustActivityControllerInterpolators();
-        }
-        mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
-
-        if (QUICKSTEP_SPRINGS.get()) {
-            mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
-        }
-        mLauncherTransitionController.getAnimationPlayer().start();
-        mHasLauncherTransitionControllerStarted = true;
-    }
-
-    /**
-     * Creates an animation that transforms the current app window into the home app.
-     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
-     * @param homeAnimationFactory The home animation factory.
-     */
-    @Override
-    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
-            HomeAnimationFactory homeAnimationFactory) {
-        RectFSpringAnim anim =
-                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
-        anim.addOnUpdateListener((r, p) -> updateSysUiFlags(Math.max(p, mCurrentShift.value)));
-        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);
-                }
-                // Make sure recents is in its final state
-                maybeUpdateRecentsAttachedState(false);
-                mActivityControlHelper.onSwipeUpToHomeComplete(mActivity);
-            }
-        });
-        return anim;
-    }
-
-    @Override
-    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
-        if (mGestureEndTarget != null) {
-            sharedState.canGestureBeContinued = mGestureEndTarget.canBeContinued;
-            sharedState.goingToLauncher = mGestureEndTarget.isLauncher;
-        }
-
-        if (sharedState.canGestureBeContinued) {
-            cancelCurrentAnimation(sharedState);
-        } else {
-            reset();
-        }
-    }
-
-    @UiThread
-    private void resumeLastTask() {
-        mRecentsAnimationWrapper.finish(false /* toRecents */, null);
-        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false);
-        doLogGesture(LAST_TASK);
-        reset();
-    }
-
-    @UiThread
-    private void startNewTask() {
-        startNewTask(STATE_HANDLER_INVALIDATED, success -> {
-            if (!success) {
-                // We couldn't launch the task, so take user to overview so they can
-                // decide what to do instead of staying in this broken state.
-                endLauncherTransitionController();
-                updateSysUiFlags(1 /* windowProgress == overview */);
-            }
-            doLogGesture(NEW_TASK);
-        });
-    }
-
-    private void reset() {
-        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-    }
-
-    /**
-     * Cancels any running animation so that the active target can be overriden by a new swipe
-     * handle (in case of quick switch).
-     */
-    private void cancelCurrentAnimation(SwipeSharedState sharedState) {
-        mCanceled = true;
-        mCurrentShift.cancelAnimation();
-        if (mLauncherTransitionController != null && mLauncherTransitionController
-                .getAnimationPlayer().isStarted()) {
-            mLauncherTransitionController.getAnimationPlayer().cancel();
-        }
-
-        if (mFinishingRecentsAnimationForNewTaskId != -1) {
-            // If we are canceling mid-starting a new task, switch to the screenshot since the
-            // recents animation has finished
-            switchToScreenshot();
-            TaskView newRunningTaskView = mRecentsView.getTaskView(
-                    mFinishingRecentsAnimationForNewTaskId);
-            int newRunningTaskId = newRunningTaskView != null
-                    ? newRunningTaskView.getTask().key.id
-                    : -1;
-            mRecentsView.setCurrentTask(newRunningTaskId);
-            sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
-        }
-    }
-
-    private void invalidateHandler() {
-        endRunningWindowAnim(false /* cancel */);
-
-        if (mGestureEndCallback != null) {
-            mGestureEndCallback.run();
-        }
-
-        mActivityInitListener.unregister();
-        mTaskSnapshot = null;
-    }
-
-    private void invalidateHandlerWithLauncher() {
-        endLauncherTransitionController();
-
-        mRecentsView.onGestureAnimationEnd();
-
-        mActivity.getRootView().setOnApplyWindowInsetsListener(null);
-        removeLiveTileOverlay();
-    }
-
-    private void endLauncherTransitionController() {
-        setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
-        if (mLauncherTransitionController != null) {
-            mLauncherTransitionController.getAnimationPlayer().end();
-            mLauncherTransitionController = null;
-        }
-    }
-
-    private void notifyTransitionCancelled() {
-        mAnimationFactory.onTransitionCancelled();
-    }
-
-    private void resetStateForAnimationCancel() {
-        boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
-        mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible);
-
-        // Leave the pending invisible flag, as it may be used by wallpaper open animation.
-        mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
-    }
-
-    private void switchToScreenshot() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-        } else if (!mRecentsAnimationWrapper.hasTargets()) {
-            // If there are no targets, then we don't need to capture anything
-            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-        } else {
-            boolean finishTransitionPosted = false;
-            SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
-            if (controller != null) {
-                // Update the screenshot of the task
-                if (mTaskSnapshot == null) {
-                    mTaskSnapshot = controller.screenshotTask(mRunningTaskId);
-                }
-                final TaskView taskView;
-                if (mGestureEndTarget == HOME) {
-                    // Capture the screenshot before finishing the transition to home to ensure it's
-                    // taken in the correct orientation, but no need to update the thumbnail.
-                    taskView = null;
-                } else {
-                    taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
-                }
-                if (taskView != null && !mCanceled) {
-                    // Defer finishing the animation until the next launcher frame with the
-                    // new thumbnail
-                    finishTransitionPosted = new WindowCallbacksCompat(taskView) {
-
-                        // The number of frames to defer until we actually finish the animation
-                        private int mDeferFrameCount = 2;
-
-                        @Override
-                        public void onPostDraw(Canvas canvas) {
-                            // If we were cancelled after this was attached, do not update
-                            // the state.
-                            if (mCanceled) {
-                                detach();
-                                return;
-                            }
-
-                            if (mDeferFrameCount > 0) {
-                                mDeferFrameCount--;
-                                // Workaround, detach and reattach to invalidate the root node for
-                                // another draw
-                                detach();
-                                attach();
-                                taskView.invalidate();
-                                return;
-                            }
-
-                            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-                            detach();
-                        }
-                    }.attach();
-                }
-            }
-            if (!finishTransitionPosted) {
-                // If we haven't posted a draw callback, set the state immediately.
-                RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, ENTER);
-                setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-                RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, EXIT);
-            }
-        }
-    }
-
-    private void finishCurrentTransitionToRecents() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-        } else if (!mRecentsAnimationWrapper.hasTargets()) {
-            // If there are no targets, then there is nothing to finish
-            setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-        } else {
-            synchronized (mRecentsAnimationWrapper) {
-                mRecentsAnimationWrapper.finish(true /* toRecents */,
-                        () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
-            }
-        }
-        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
-    }
-
-    private void finishCurrentTransitionToHome() {
-        synchronized (mRecentsAnimationWrapper) {
-            mRecentsAnimationWrapper.finish(true /* toRecents */,
-                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
-                    true /* sendUserLeaveHint */);
-        }
-        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
-        doLogGesture(HOME);
-    }
-
-    private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
-        endLauncherTransitionController();
-        mActivityControlHelper.onSwipeUpToRecentsComplete(mActivity);
-        mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */,
-                true /* screenshot */);
-        mRecentsView.onSwipeUpAnimationSuccess();
-
-        RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
-
-        doLogGesture(RECENTS);
-        reset();
-    }
-
-    private void setTargetAlphaProvider(TargetAlphaProvider provider) {
-        mClipAnimationHelper.setTaskAlphaCallback(provider);
-        updateFinalShift();
-    }
-
-    private synchronized void addLiveTileOverlay() {
-        if (!mLiveTileOverlayAttached) {
-            mActivity.getRootView().getOverlay().add(mLiveTileOverlay);
-            mRecentsView.setLiveTileOverlay(mLiveTileOverlay);
-            mLiveTileOverlayAttached = true;
-        }
-    }
-
-    private synchronized void removeLiveTileOverlay() {
-        if (mLiveTileOverlayAttached) {
-            mActivity.getRootView().getOverlay().remove(mLiveTileOverlay);
-            mRecentsView.setLiveTileOverlay(null);
-            mLiveTileOverlayAttached = false;
-        }
-    }
-
-    public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, float expectedAlpha) {
-        if (!isNotInRecents(app)) {
-            return 0;
-        }
-        return expectedAlpha;
-    }
-
-    private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
-        return app.isNotInRecents
-                || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
new file mode 100644
index 0000000..be3fdde
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+
+/**
+ * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
+ */
+public class FallbackNavBarTouchController implements TouchController,
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+
+    private final RecentsActivity mActivity;
+    @Nullable
+    private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+
+    public FallbackNavBarTouchController(RecentsActivity activity) {
+        mActivity = activity;
+        SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
+        if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
+            NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
+                    DefaultDisplay.INSTANCE.get(mActivity).getInfo());
+            mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
+                    true /* disableHorizontalSwipe */, navBarPosition,
+                    null /* onInterceptTouch */, this);
+        } else {
+            mTriggerSwipeUpTracker = null;
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+        if (cameFromNavBar && mTriggerSwipeUpTracker != null) {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                mTriggerSwipeUpTracker.init();
+            }
+            onControllerTouchEvent(ev);
+            return mTriggerSwipeUpTracker.interceptedTouch();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        if (mTriggerSwipeUpTracker != null) {
+            mTriggerSwipeUpTracker.onMotionEvent(ev);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+        mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
+    }
+
+    @Override
+    public void onSwipeUpCancelled() {}
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
new file mode 100644
index 0000000..3f1e7ba
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
+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.PLAY_ATOMIC_OVERVIEW_PEEK;
+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.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.ClearAllButton;
+
+/**
+ * State controller for fallback recents activity
+ */
+public class FallbackRecentsStateController implements StateHandler<RecentsState> {
+
+    private final StateAnimationConfig mNoConfig = new StateAnimationConfig();
+    private final RecentsActivity mActivity;
+    private final FallbackRecentsView mRecentsView;
+
+    public FallbackRecentsStateController(RecentsActivity activity) {
+        mActivity = activity;
+        mRecentsView = activity.getOverviewPanel();
+    }
+
+    @Override
+    public void setState(RecentsState state) {
+        mRecentsView.updateEmptyMessage();
+        mRecentsView.resetTaskVisuals();
+        setProperties(state, mNoConfig, PropertySetter.NO_ANIM_PROPERTY_SETTER);
+    }
+
+    @Override
+    public void setStateWithAnimation(RecentsState toState, StateAnimationConfig config,
+            PendingAnimation setter) {
+        if (!config.hasAnimationFlag(PLAY_ATOMIC_OVERVIEW_PEEK | PLAY_ATOMIC_OVERVIEW_SCALE)) {
+            // The entire recents animation is played atomically.
+            return;
+        }
+        if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
+            return;
+        }
+        // While animating into recents, update the visible task data as needed
+        setter.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
+        mRecentsView.updateEmptyMessage();
+
+        setProperties(toState, config, setter);
+    }
+
+    private void setProperties(RecentsState state, StateAnimationConfig config,
+            PropertySetter setter) {
+        float buttonAlpha = state.hasButtons() ? 1 : 0;
+        setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
+                buttonAlpha, LINEAR);
+        setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
+                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+
+        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+                config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
+        setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
+                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+
+        setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
+                config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+        setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 7f1aae5..d20bbe9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -15,21 +15,20 @@
  */
 package com.android.quickstep.fallback;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
 
+import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
+import android.os.Build;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
 
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -37,36 +36,24 @@
 
 import java.util.ArrayList;
 
-public class FallbackRecentsView extends RecentsView<RecentsActivity> {
+@TargetApi(Build.VERSION_CODES.R)
+public class FallbackRecentsView extends RecentsView<RecentsActivity>
+        implements StateListener<RecentsState> {
 
-    public static final FloatProperty<FallbackRecentsView> ZOOM_PROGRESS =
-            new FloatProperty<FallbackRecentsView> ("zoomInProgress") {
-
-                @Override
-                public void setValue(FallbackRecentsView view, float value) {
-                    view.setZoomProgress(value);
-                }
-
-                @Override
-                public Float get(FallbackRecentsView view) {
-                    return view.mZoomInProgress;
-                }
-            };
-
-    private float mZoomInProgress = 0;
-    private boolean mInOverviewState = true;
-
-    private float mZoomScale = 1f;
-    private float mZoomTranslationY = 0f;
-
-    private RunningTaskInfo mRunningTaskInfo;
+    private RunningTaskInfo mHomeTaskInfo;
 
     public FallbackRecentsView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
     public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
+        super(context, attrs, defStyleAttr, FallbackActivityInterface.INSTANCE);
+        mActivity.getStateManager().addStateListener(this);
+    }
+
+    @Override
+    public void init(OverviewActionsView actionsView) {
+        super.init(actionsView);
         setOverviewStateEnabled(true);
         setOverlayEnabled(true);
     }
@@ -77,107 +64,65 @@
     }
 
     @Override
-    public void onViewAdded(View child) {
-        super.onViewAdded(child);
-        updateEmptyMessage();
-    }
-
-    @Override
-    public void onViewRemoved(View child) {
-        super.onViewRemoved(child);
-        updateEmptyMessage();
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        maybeDrawEmptyMessage(canvas);
-        super.draw(canvas);
-    }
-
-    @Override
-    public void reset() {
-        super.reset();
-        resetViewUI();
-    }
-
-    @Override
-    protected void getTaskSize(DeviceProfile dp, Rect outRect) {
-        LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect);
-    }
-
-    @Override
     public boolean shouldUseMultiWindowTaskSizeStrategy() {
         // Just use the activity task size for multi-window as well.
         return false;
     }
 
-    public void resetViewUI() {
-        setZoomProgress(0);
-        resetTaskVisuals();
+    /**
+     * When starting gesture interaction from home, we add a temporary invisible tile corresponding
+     * to the home task. This allows us to handle quick-switch similarly to a quick-switching
+     * from a foreground task.
+     */
+    public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
+        mHomeTaskInfo = homeTaskInfo;
+        onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
     }
 
-    public void setInOverviewState(boolean inOverviewState) {
-        if (mInOverviewState != inOverviewState) {
-            mInOverviewState = inOverviewState;
-            if (mInOverviewState) {
-                resetTaskVisuals();
-            } else {
-                setZoomProgress(1);
+    /**
+     * When the gesture ends and recents view become interactive, we also remove the temporary
+     * invisible tile added for the home task. This also pushes the remaining tiles back
+     * to the center.
+     */
+    @Override
+    public void onGestureAnimationEnd() {
+        super.onGestureAnimationEnd();
+        if (mHomeTaskInfo != null) {
+            TaskView tv = getTaskView(mHomeTaskInfo.taskId);
+            if (tv != null) {
+                PendingAnimation pa = createTaskDismissAnimation(tv, true, false, 150);
+                pa.addEndListener(e -> setCurrentTask(-1));
+                runDismissAnimation(pa);
             }
         }
     }
 
     @Override
-    public void resetTaskVisuals() {
-        super.resetTaskVisuals();
-        setFullscreenProgress(mFullscreenProgress);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-
-        if (getTaskViewCount() == 0) {
-            mZoomScale = 1f;
-            mZoomTranslationY = 0f;
-        } else {
-            TaskView dummyTask = getTaskViewAt(0);
-            ScaleAndTranslation sat = getTempClipAnimationHelper()
-                    .updateForFullscreenOverview(dummyTask)
-                    .getScaleAndTranslation();
-            mZoomScale = sat.scale;
-            mZoomTranslationY = sat.translationY;
-        }
-
-        setZoomProgress(mZoomInProgress);
-    }
-
-    public void setZoomProgress(float progress) {
-        mZoomInProgress = progress;
-        SCALE_PROPERTY.set(this, Utilities.mapRange(mZoomInProgress, 1, mZoomScale));
-        TRANSLATION_Y.set(this, Utilities.mapRange(mZoomInProgress, 0, mZoomTranslationY));
-        FULLSCREEN_PROGRESS.set(this, mZoomInProgress);
-    }
-
-    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
-        mRunningTaskInfo = runningTaskInfo;
-        onGestureAnimationStart(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
-    }
-
-    @Override
     public void setCurrentTask(int runningTaskId) {
         super.setCurrentTask(runningTaskId);
-        if (mRunningTaskInfo != null && mRunningTaskInfo.taskId != runningTaskId) {
-            mRunningTaskInfo = null;
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId != runningTaskId) {
+            mHomeTaskInfo = null;
+            setRunningTaskHidden(false);
         }
     }
 
     @Override
+    protected boolean shouldAddDummyTaskView(int runningTaskId) {
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
+                && getTaskViewCount() == 0) {
+            // Do not add a dummy task if we are running over home with empty recents, so that we
+            // show the empty recents message instead of showing a dummy task and later removing it.
+            return false;
+        }
+        return super.shouldAddDummyTaskView(runningTaskId);
+    }
+
+    @Override
     protected void applyLoadPlan(ArrayList<Task> tasks) {
         // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
         // 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 (mRunningTaskInfo != null && mRunningTaskInfo.taskId == mRunningTaskId) {
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
             // Check if the task list has running task
             boolean found = false;
             for (Task t : tasks) {
@@ -189,10 +134,52 @@
             if (!found) {
                 ArrayList<Task> newList = new ArrayList<>(tasks.size() + 1);
                 newList.addAll(tasks);
-                newList.add(Task.from(new TaskKey(mRunningTaskInfo), mRunningTaskInfo, false));
+                newList.add(Task.from(new TaskKey(mHomeTaskInfo), mHomeTaskInfo, false));
                 tasks = newList;
             }
         }
         super.applyLoadPlan(tasks);
     }
+
+    @Override
+    public void setRunningTaskHidden(boolean isHidden) {
+        if (mHomeTaskInfo != null) {
+            // Always keep the home task hidden
+            isHidden = true;
+        }
+        super.setRunningTaskHidden(isHidden);
+    }
+
+    @Override
+    public void setModalStateEnabled(boolean isModalState) {
+        super.setModalStateEnabled(isModalState);
+        if (isModalState) {
+            mActivity.getStateManager().goToState(RecentsState.MODAL_TASK);
+        } else {
+            if (mActivity.isInState(RecentsState.MODAL_TASK)) {
+                mActivity.getStateManager().goToState(DEFAULT);
+            }
+        }
+    }
+
+    @Override
+    public void onStateTransitionStart(RecentsState toState) {
+        setOverviewStateEnabled(true);
+        setFreezeViewVisibility(true);
+    }
+
+    @Override
+    public void onStateTransitionComplete(RecentsState finalState) {
+        setOverlayEnabled(finalState == DEFAULT || finalState == MODAL_TASK);
+        setFreezeViewVisibility(false);
+    }
+
+    @Override
+    public void setOverviewStateEnabled(boolean enabled) {
+        super.setOverviewStateEnabled(enabled);
+        if (enabled) {
+            RecentsState state = mActivity.getStateManager().getState();
+            setDisallowScrollToClearAll(!state.hasButtons());
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
new file mode 100644
index 0000000..a00015a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.quickstep.fallback;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.RecentsActivity;
+
+/**
+ * Drag layer for fallback recents activity
+ */
+public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
+
+    public RecentsDragLayer(Context context, AttributeSet attrs) {
+        super(context, attrs, 1 /* alphaChannelCount */);
+    }
+
+    @Override
+    public void recreateControllers() {
+        mControllers = new TouchController[] {
+                new RecentsTaskController(mActivity),
+                new FallbackNavBarTouchController(mActivity),
+        };
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        super.setInsets(insets);
+        setBackground(insets.top == 0  || !mAllowSysuiScrims
+                ? null
+                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
deleted file mode 100644
index 2b369e7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.R;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
-
-public class RecentsRootView extends BaseDragLayer<RecentsActivity> {
-
-    private static final int MIN_SIZE = 10;
-    private final RecentsActivity mActivity;
-
-    private final Point mLastKnownSize = new Point(MIN_SIZE, MIN_SIZE);
-
-    public RecentsRootView(Context context, AttributeSet attrs) {
-        super(context, attrs, 1 /* alphaChannelCount */);
-        mActivity = BaseActivity.fromContext(context);
-        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | SYSTEM_UI_FLAG_LAYOUT_STABLE);
-    }
-
-    public Point getLastKnownSize() {
-        return mLastKnownSize;
-    }
-
-    public void setup() {
-        mControllers = new TouchController[] { new RecentsTaskController(mActivity) };
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // Check size changes before the actual measure, to avoid multiple measure calls.
-        int width = Math.max(MIN_SIZE, MeasureSpec.getSize(widthMeasureSpec));
-        int height = Math.max(MIN_SIZE, MeasureSpec.getSize(heightMeasureSpec));
-        if (mLastKnownSize.x != width || mLastKnownSize.y != height) {
-            mLastKnownSize.set(width, height);
-            mActivity.onRootViewSizeChanged();
-        }
-
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    @TargetApi(23)
-    @Override
-    protected boolean fitSystemWindows(Rect insets) {
-        // Update device profile before notifying the children.
-        mActivity.getDeviceProfile().updateInsets(insets);
-        setInsets(insets);
-        return false; // Let children get the full insets
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by
-        // modifying child layout params.
-        if (!insets.equals(mInsets)) {
-            super.setInsets(insets);
-        }
-        setBackground(insets.top == 0  || !mAllowSysuiScrims
-                ? null
-                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
-    }
-
-    public void dispatchInsets() {
-        mActivity.getDeviceProfile().updateInsets(mInsets);
-        super.setInsets(mInsets);
-    }
-}
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
new file mode 100644
index 0000000..211a30c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
+import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
+
+import android.content.Context;
+
+import com.android.launcher3.statemanager.BaseState;
+import com.android.quickstep.RecentsActivity;
+
+/**
+ * State definition for Fallback recents
+ */
+public class RecentsState implements BaseState<RecentsState> {
+
+    private static final int FLAG_MODAL = BaseState.getFlag(0);
+    private static final int FLAG_HAS_BUTTONS = BaseState.getFlag(1);
+    private static final int FLAG_FULL_SCREEN = BaseState.getFlag(2);
+
+    public static final RecentsState DEFAULT = new RecentsState(0, FLAG_HAS_BUTTONS);
+    public static final RecentsState MODAL_TASK = new ModalState(1,
+            FLAG_DISABLE_RESTORE | FLAG_HAS_BUTTONS | FLAG_MODAL);
+    public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
+            FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN);
+
+    public final int ordinal;
+    private final int mFlags;
+
+    private static final float NO_OFFSET = 0;
+    private static final float NO_SCALE = 1;
+
+    public RecentsState(int id, int flags) {
+        this.ordinal = id;
+        this.mFlags = flags;
+    }
+
+
+    @Override
+    public String toString() {
+        return "Ordinal-" + ordinal;
+    }
+
+    @Override
+    public final boolean hasFlag(int mask) {
+        return (mFlags & mask) != 0;
+    }
+
+    @Override
+    public int getTransitionDuration(Context context) {
+        return 250;
+    }
+
+    @Override
+    public RecentsState getHistoryForState(RecentsState previousState) {
+        return DEFAULT;
+    }
+
+    /**
+     * For this state, how modal should over view been shown. 0 modalness means all tasks drawn,
+     * 1 modalness means the current task is show on its own.
+     */
+    public float getOverviewModalness() {
+        return hasFlag(FLAG_MODAL) ? 1 : 0;
+    }
+
+    public boolean isFullScreen() {
+        return hasFlag(FLAG_FULL_SCREEN);
+    }
+
+    public boolean hasButtons() {
+        return hasFlag(FLAG_HAS_BUTTONS);
+    }
+
+    public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+        return new float[] { NO_SCALE, NO_OFFSET };
+    }
+
+
+    private static class ModalState extends RecentsState {
+
+        public ModalState(int id, int flags) {
+            super(id, flags);
+        }
+
+        @Override
+        public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+            return getOverviewScaleAndOffsetForModalState(activity);
+        }
+    }
+
+    private static class BackgroundAppState extends RecentsState {
+        public BackgroundAppState(int id, int flags) {
+            super(id, flags);
+        }
+
+        @Override
+        public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+            return getOverviewScaleAndOffsetForBackgroundState(activity);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
index a113604..d7458d2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
@@ -28,4 +28,9 @@
     protected boolean isRecentsInteractive() {
         return mActivity.hasWindowFocus();
     }
+
+    @Override
+    protected boolean isRecentsModal() {
+        return false;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 1f73a28..5ad48eb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -23,31 +23,29 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import android.content.Context;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.Log;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.R;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.MotionPauseDetector;
-import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
- * Touch consumer for two finger swipe actions for accessibility actions
+ * Input consumer for two finger swipe actions for accessibility actions
  */
 public class AccessibilityInputConsumer extends DelegateInputConsumer {
 
     private static final String TAG = "A11yInputConsumer";
 
-    private final ISystemUiProxy mSystemUiProxy;
+    private final Context mContext;
     private final VelocityTracker mVelocityTracker;
     private final MotionPauseDetector mMotionPauseDetector;
-    private final boolean mAllowLongClick;
-    private final RectF mSwipeTouchRegion;
+    private final RecentsAnimationDeviceState mDeviceState;
 
     private final float mMinGestureDistance;
     private final float mMinFlingVelocity;
@@ -56,19 +54,17 @@
     private float mDownY;
     private float mTotalY;
 
-    public AccessibilityInputConsumer(Context context, ISystemUiProxy systemUiProxy,
-            boolean allowLongClick, InputConsumer delegate, InputMonitorCompat inputMonitor,
-            RectF swipeTouchRegion) {
+    public AccessibilityInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
-        mSystemUiProxy = systemUiProxy;
+        mContext = context;
         mVelocityTracker = VelocityTracker.obtain();
         mMinGestureDistance = context.getResources()
                 .getDimension(R.dimen.accessibility_gesture_min_swipe_distance);
         mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
-        mSwipeTouchRegion = swipeTouchRegion;
+        mDeviceState = deviceState;
 
         mMotionPauseDetector = new MotionPauseDetector(context);
-        mAllowLongClick = allowLongClick;
     }
 
     @Override
@@ -103,7 +99,7 @@
             case ACTION_POINTER_DOWN: {
                 if (mState == STATE_INACTIVE) {
                     int pointerIndex = ev.getActionIndex();
-                    if (mSwipeTouchRegion.contains(ev.getX(pointerIndex), ev.getY(pointerIndex))
+                    if (mDeviceState.isInSwipeUpTouchRegion(ev, pointerIndex)
                             && mDelegate.allowInterceptByParent()) {
                         setActive(ev);
 
@@ -116,34 +112,29 @@
                 break;
             }
             case ACTION_MOVE: {
-                if (mState == STATE_ACTIVE && mAllowLongClick) {
+                if (mState == STATE_ACTIVE && mDeviceState.isAccessibilityMenuShortcutAvailable()) {
                     int pointerIndex = ev.findPointerIndex(mActivePointerId);
                     if (pointerIndex == -1) {
                         break;
                     }
-
-                    mMotionPauseDetector.addPosition(ev.getY(pointerIndex) - mDownY,
-                            ev.getEventTime());
+                    mMotionPauseDetector.addPosition(ev, pointerIndex);
                 }
                 break;
             }
             case ACTION_UP:
                 if (mState == STATE_ACTIVE) {
-                    try {
-                        if (mAllowLongClick && mMotionPauseDetector.isPaused()) {
-                            mSystemUiProxy.notifyAccessibilityButtonLongClicked();
-                        } else {
-                            mTotalY += (ev.getY() - mDownY);
-                            mVelocityTracker.computeCurrentVelocity(1000);
+                    if (mDeviceState.isAccessibilityMenuShortcutAvailable()
+                            && mMotionPauseDetector.isPaused()) {
+                        SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonLongClicked();
+                    } else {
+                        mTotalY += (ev.getY() - mDownY);
+                        mVelocityTracker.computeCurrentVelocity(1000);
 
-                            if ((-mTotalY) > mMinGestureDistance
-                                    || (-mVelocityTracker.getYVelocity()) > mMinFlingVelocity) {
-                                mSystemUiProxy.notifyAccessibilityButtonClicked(
-                                        Display.DEFAULT_DISPLAY);
-                            }
+                        if ((-mTotalY) > mMinGestureDistance
+                                || (-mVelocityTracker.getYVelocity()) > mMinFlingVelocity) {
+                            SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonClicked(
+                                    Display.DEFAULT_DISPLAY);
                         }
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Unable to notify accessibility event", e);
                     }
                 }
                 // Follow through
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
similarity index 78%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
index d4cdaf0..89e6931 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantTouchConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
@@ -37,9 +37,7 @@
 import android.content.res.Resources;
 import android.graphics.PointF;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.HapticFeedbackConstants;
@@ -50,16 +48,18 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.quickstep.ActivityControlHelper;
-import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
  * Touch consumer for handling events to launch assistant from launcher
  */
-public class AssistantTouchConsumer extends DelegateInputConsumer {
+public class AssistantInputConsumer extends DelegateInputConsumer {
 
-    private static final String TAG = "AssistantTouchConsumer";
+    private static final String TAG = "AssistantInputConsumer";
     private static final long RETRACT_ANIMATION_DURATION_MS = 300;
 
     // From //java/com/google/android/apps/gsa/search/shared/util/OpaContract.java.
@@ -81,25 +81,26 @@
     private long mDragTime;
     private float mLastProgress;
     private int mDirection;
-    private ActivityControlHelper mActivityControlHelper;
+    private BaseActivityInterface mActivityInterface;
 
     private final float mDragDistThreshold;
     private final float mFlingDistThreshold;
     private final long mTimeThreshold;
     private final int mAngleThreshold;
     private final float mSquaredSlop;
-    private final ISystemUiProxy mSysUiProxy;
     private final Context mContext;
     private final GestureDetector mGestureDetector;
     private final boolean mIsAssistGestureConstrained;
 
-    public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
-            ActivityControlHelper activityControlHelper, InputConsumer delegate,
-        InputMonitorCompat inputMonitor, boolean isAssistGestureConstrained) {
+    public AssistantInputConsumer(
+            Context context,
+            GestureState gestureState,
+            InputConsumer delegate,
+            InputMonitorCompat inputMonitor,
+            boolean isAssistGestureConstrained) {
         super(delegate, inputMonitor);
         final Resources res = context.getResources();
         mContext = context;
-        mSysUiProxy = systemUiProxy;
         mIsAssistGestureConstrained = isAssistGestureConstrained;
         mDragDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
         mFlingDistThreshold = res.getDimension(R.dimen.gestures_assistant_fling_threshold);
@@ -109,7 +110,7 @@
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
 
         mSquaredSlop = slop * slop;
-        mActivityControlHelper = activityControlHelper;
+        mActivityInterface = gestureState.getActivityInterface();
 
         mGestureDetector = new GestureDetector(context, new AssistantGestureListener());
     }
@@ -133,8 +134,8 @@
             case ACTION_POINTER_DOWN: {
                 if (mState != STATE_ACTIVE) {
                     mState = STATE_DELEGATE_ACTIVE;
-                    break;
                 }
+                break;
             }
             case ACTION_POINTER_UP: {
                 int ptrIdx = ev.getActionIndex();
@@ -200,13 +201,7 @@
                         SWIPE_NOOP, mDirection, NAVBAR);
                     animator.addUpdateListener(valueAnimator -> {
                         float progress = (float) valueAnimator.getAnimatedValue();
-                        try {
-
-                            mSysUiProxy.onAssistantProgress(progress);
-                        } catch (RemoteException e) {
-                            Log.w(TAG, "Failed to send SysUI start/send assistant progress: "
-                                + progress, e);
-                        }
+                        SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(progress);
                     });
                     animator.setInterpolator(Interpolators.DEACCEL_2);
                     animator.start();
@@ -226,22 +221,17 @@
     private void updateAssistantProgress() {
         if (!mLaunchedAssistant) {
             mLastProgress = Math.min(mDistance * 1f / mDragDistThreshold, 1) * mTimeFraction;
-            try {
-                if (mDistance >= mDragDistThreshold && mTimeFraction >= 1) {
-                    mSysUiProxy.onAssistantGestureCompletion(0);
-                    startAssistantInternal(SWIPE);
+            if (mDistance >= mDragDistThreshold && mTimeFraction >= 1) {
+                SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(0);
+                startAssistantInternal(SWIPE);
 
-                    Bundle args = new Bundle();
-                    args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE);
-                    args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
-                    mSysUiProxy.startAssistant(args);
-                    mLaunchedAssistant = true;
-                } else {
-                    mSysUiProxy.onAssistantProgress(mLastProgress);
-                }
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + mLastProgress,
-                    e);
+                Bundle args = new Bundle();
+                args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE);
+                args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+                SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
+                mLaunchedAssistant = true;
+            } else {
+                SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(mLastProgress);
             }
         }
     }
@@ -250,8 +240,7 @@
         UserEventDispatcher.newInstance(mContext)
             .logActionOnContainer(gestureType, mDirection, NAVBAR);
 
-        BaseDraggingActivity launcherActivity = mActivityControlHelper
-            .getCreatedActivity();
+        BaseDraggingActivity launcherActivity = mActivityInterface.getCreatedActivity();
         if (launcherActivity != null) {
             launcherActivity.getRootView().performHapticFeedback(
                 13, // HapticFeedbackConstants.GESTURE_END
@@ -276,25 +265,19 @@
         @Override
         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
             if (!mIsAssistGestureConstrained
-                    && isValidAssistantGestureAngle(velocityX, -velocityY)
+                && isValidAssistantGestureAngle(velocityX, -velocityY)
                     && mDistance >= mFlingDistThreshold
                     && !mLaunchedAssistant
                     && mState != STATE_DELEGATE_ACTIVE) {
                 mLastProgress = 1;
-                try {
-                    mSysUiProxy.onAssistantGestureCompletion(
-                        (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
-                    startAssistantInternal(FLING);
+                SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(
+                    (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
+                startAssistantInternal(FLING);
 
-                    Bundle args = new Bundle();
-                    args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
-                    mSysUiProxy.startAssistant(args);
-                    mLaunchedAssistant = true;
-                } catch (RemoteException e) {
-                    Log.w(TAG,
-                        "Failed to send SysUI start/send assistant progress: " + mLastProgress,
-                        e);
-                }
+                Bundle args = new Bundle();
+                args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+                SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
+                mLaunchedAssistant = true;
             }
             return true;
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 311ddd2..67a15a7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -2,6 +2,9 @@
 
 import android.view.MotionEvent;
 
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.quickstep.InputConsumer;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public abstract class DelegateInputConsumer implements InputConsumer {
@@ -22,8 +25,11 @@
     }
 
     @Override
-    public boolean useSharedSwipeState() {
-        return mDelegate.useSharedSwipeState();
+    public InputConsumer getActiveConsumerInHierarchy() {
+        if (mState == STATE_ACTIVE) {
+            return this;
+        }
+        return mDelegate.getActiveConsumerInHierarchy();
     }
 
     @Override
@@ -38,6 +44,7 @@
 
     protected void setActive(MotionEvent ev) {
         mState = STATE_ACTIVE;
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitor.pilferPointers();
 
         // Send cancel event
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index b24c788..3a97216 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -21,41 +21,49 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.TouchInteractionService.INTENT_EXTRA_LOG_TRACE_ID;
-import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
-import android.content.ComponentName;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.LockScreenRecentsActivity;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
 import com.android.quickstep.MultiStateCallback;
-import com.android.quickstep.SwipeSharedState;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.RecentsAnimationListenerSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
 
 /**
  * A dummy input consumer used when the device is still locked, e.g. from secure camera.
  */
 public class DeviceLockedInputConsumer implements InputConsumer,
-        SwipeAnimationTargetSet.SwipeAnimationListener {
-
-    private static final float SCALE_DOWN = 0.75f;
+        RecentsAnimationCallbacks.RecentsAnimationListener, BuilderProxy {
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
     private static int getFlagForIndex(int index, String name) {
@@ -71,45 +79,47 @@
             getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
 
     private final Context mContext;
+    private final RecentsAnimationDeviceState mDeviceState;
+    private final TaskAnimationManager mTaskAnimationManager;
+    private final GestureState mGestureState;
     private final float mTouchSlopSquared;
-    private final SwipeSharedState mSwipeSharedState;
     private final InputMonitorCompat mInputMonitorCompat;
 
     private final PointF mTouchDown = new PointF();
-    private final ClipAnimationHelper mClipAnimationHelper;
-    private int mLogId;
-    private final ClipAnimationHelper.TransformParams mTransformParams;
-    private final Point mDisplaySize;
+    private final TransformParams mTransformParams;
     private final MultiStateCallback mStateCallback;
-    private final RectF mSwipeTouchRegion;
-    public final int mRunningTaskId;
+
+    private final Point mDisplaySize;
+    private final Matrix mMatrix = new Matrix();
+    private final float mMaxTranslationY;
 
     private VelocityTracker mVelocityTracker;
-    private float mProgress;
+    private final AnimatedFloat mProgress = new AnimatedFloat(this::applyTransform);
 
     private boolean mThresholdCrossed = false;
+    private boolean mHomeLaunched = false;
 
-    private SwipeAnimationTargetSet mTargetSet;
+    private RecentsAnimationController mRecentsAnimationController;
 
-    public DeviceLockedInputConsumer(Context context, SwipeSharedState swipeSharedState,
-            InputMonitorCompat inputMonitorCompat, RectF swipeTouchRegion, int runningTaskId,
-            int logId) {
+    public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState,
+            InputMonitorCompat inputMonitorCompat) {
         mContext = context;
+        mDeviceState = deviceState;
+        mTaskAnimationManager = taskAnimationManager;
+        mGestureState = gestureState;
         mTouchSlopSquared = squaredTouchSlop(context);
-        mSwipeSharedState = swipeSharedState;
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-        mLogId = logId;
-        mTransformParams = new ClipAnimationHelper.TransformParams();
+        mTransformParams = new TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
-        mSwipeTouchRegion = swipeTouchRegion;
-        mRunningTaskId = runningTaskId;
+        mMaxTranslationY = context.getResources().getDimensionPixelSize(
+                R.dimen.device_locked_y_offset);
 
         // Do not use DeviceProfile as the user data might be locked
         mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
 
         // Init states
         mStateCallback = new MultiStateCallback(STATE_NAMES);
-        mStateCallback.addCallback(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
+        mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
                 this::endRemoteAnimation);
 
         mVelocityTracker = VelocityTracker.obtain();
@@ -137,7 +147,7 @@
                 if (!mThresholdCrossed) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         int action = ev.getAction();
                         ev.setAction(ACTION_CANCEL);
                         finishTouchTracking(ev);
@@ -153,11 +163,7 @@
                     }
                 } else {
                     float dy = Math.max(mTouchDown.y - y, 0);
-                    mProgress = dy / mDisplaySize.y;
-                    mTransformParams.setProgress(mProgress);
-                    if (mTargetSet != null) {
-                        mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
-                    }
+                    mProgress.updateValue(dy / mDisplaySize.y);
                 }
                 break;
             }
@@ -173,7 +179,6 @@
      * the animation can still be running.
      */
     private void finishTouchTracking(MotionEvent ev) {
-        mStateCallback.setState(STATE_HANDLER_INVALIDATED);
         if (mThresholdCrossed && ev.getAction() == ACTION_UP) {
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
@@ -187,14 +192,29 @@
                 // Is fling
                 dismissTask = velocityY < 0;
             } else {
-                dismissTask = mProgress >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
+                dismissTask = mProgress.value >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
             }
-            if (dismissTask) {
-                // For now, just start the home intent so user is prompted to unlock the device.
-                mContext.startActivity(new Intent(Intent.ACTION_MAIN)
-                        .addCategory(Intent.CATEGORY_HOME)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-            }
+
+            // Animate back to fullscreen before finishing
+            ObjectAnimator animator = mProgress.animateToValue(mProgress.value, 0);
+            animator.setDuration(100);
+            animator.setInterpolator(Interpolators.ACCEL);
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (dismissTask) {
+                        // For now, just start the home intent so user is prompted to unlock the device.
+                        mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+                                .addCategory(Intent.CATEGORY_HOME)
+                                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+                        mHomeLaunched = true;
+                    }
+                    mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+                }
+            });
+            animator.start();
+        } else {
+            mStateCallback.setState(STATE_HANDLER_INVALIDATED);
         }
         mVelocityTracker.recycle();
         mVelocityTracker = null;
@@ -202,49 +222,53 @@
 
     private void startRecentsTransition() {
         mThresholdCrossed = true;
-        RecentsAnimationListenerSet newListenerSet =
-                mSwipeSharedState.newRecentsAnimationListenerSet();
-        newListenerSet.addListener(this);
-        Intent intent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_DEFAULT)
-                .setComponent(new ComponentName(mContext, LockScreenRecentsActivity.class))
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
-                .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
-
+        mHomeLaunched = false;
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitorCompat.pilferPointers();
-        startRecentsActivityAsync(intent, newListenerSet);
+
+        Intent intent = mGestureState.getHomeIntent()
+                .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
+        mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
     }
 
     @Override
-    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        mTargetSet = targetSet;
-
-        Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
-        RemoteAnimationTargetCompat targetCompat = targetSet.findTask(mRunningTaskId);
-        if (targetCompat != null) {
-            mClipAnimationHelper.updateSource(displaySize, targetCompat);
-        }
-
-        Utilities.scaleRectAboutCenter(displaySize, SCALE_DOWN);
-        displaySize.offsetTo(displaySize.left, 0);
-        mClipAnimationHelper.updateTargetRect(displaySize);
-        mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
-
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        mRecentsAnimationController = controller;
+        mTransformParams.setTargetSet(targets);
+        applyTransform();
         mStateCallback.setState(STATE_TARGET_RECEIVED);
     }
 
     @Override
-    public void onRecentsAnimationCanceled() {
-        mTargetSet = null;
+    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+        mRecentsAnimationController = null;
+        mTransformParams.setTargetSet(null);
     }
 
     private void endRemoteAnimation() {
-        if (mTargetSet != null) {
-            mTargetSet.finishController(
+        if (mHomeLaunched) {
+            ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false);
+        } else if (mRecentsAnimationController != null) {
+            mRecentsAnimationController.finishController(
                     false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
         }
     }
 
+    private void applyTransform() {
+        mTransformParams.setProgress(mProgress.value);
+        if (mTransformParams.getTargetSet() != null) {
+            mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
+        }
+    }
+
+    @Override
+    public void onBuildTargetParams(
+            Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+        mMatrix.setTranslate(0, mProgress.value * mMaxTranslationY);
+        builder.withMatrix(mMatrix);
+    }
+
     @Override
     public void onConsumerAboutToBeSwitched() {
         mStateCallback.setState(STATE_HANDLER_INVALIDATED);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
deleted file mode 100644
index e0ff8af..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ /dev/null
@@ -1,456 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
-import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
-import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.HOME;
-import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.RECENTS;
-import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.os.Bundle;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
-import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.BaseSwipeUpHandler;
-import com.android.quickstep.MultiStateCallback;
-import com.android.quickstep.OverviewComponentObserver;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.SwipeSharedState;
-import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.util.ObjectWrapper;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.InputConsumerController;
-
-public class FallbackNoButtonInputConsumer extends
-        BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
-
-    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
-
-    private static int getFlagForIndex(int index, String name) {
-        if (DEBUG_STATES) {
-            STATE_NAMES[index] = name;
-        }
-        return 1 << index;
-    }
-
-    private static final int STATE_RECENTS_PRESENT =
-            getFlagForIndex(0, "STATE_RECENTS_PRESENT");
-    private static final int STATE_HANDLER_INVALIDATED =
-            getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
-
-    private static final int STATE_GESTURE_CANCELLED =
-            getFlagForIndex(2, "STATE_GESTURE_CANCELLED");
-    private static final int STATE_GESTURE_COMPLETED =
-            getFlagForIndex(3, "STATE_GESTURE_COMPLETED");
-    private static final int STATE_APP_CONTROLLER_RECEIVED =
-            getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED");
-
-    public enum GestureEndTarget {
-        HOME(3, 100, 1),
-        RECENTS(1, 300, 0),
-        LAST_TASK(0, 150, 1),
-        NEW_TASK(0, 150, 1);
-
-        private final float mEndProgress;
-        private final long mDurationMultiplier;
-        private final float mLauncherAlpha;
-
-        GestureEndTarget(float endProgress, long durationMultiplier, float launcherAlpha) {
-            mEndProgress = endProgress;
-            mDurationMultiplier = durationMultiplier;
-            mLauncherAlpha = launcherAlpha;
-        }
-    }
-
-    private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
-
-    private boolean mIsMotionPaused = false;
-    private GestureEndTarget mEndTarget;
-
-    private final boolean mInQuickSwitchMode;
-    private final boolean mContinuingLastGesture;
-    private final boolean mRunningOverHome;
-    private final boolean mSwipeUpOverHome;
-
-    private final RunningTaskInfo mRunningTaskInfo;
-
-    private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
-    private RunningWindowAnim mFinishAnimation;
-
-    public FallbackNoButtonInputConsumer(Context context,
-            OverviewComponentObserver overviewComponentObserver,
-            RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
-            InputConsumerController inputConsumer,
-            boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
-        mLauncherAlpha.value = 1;
-
-        mRunningTaskInfo = runningTaskInfo;
-        mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
-        mContinuingLastGesture = continuingLastGesture;
-        mRunningOverHome = ActivityManagerWrapper.isHomeTask(runningTaskInfo);
-        mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
-
-        if (mSwipeUpOverHome) {
-            mClipAnimationHelper.setBaseAlphaCallback((t, a) -> 1 - mLauncherAlpha.value);
-        } else {
-            mClipAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
-        }
-
-        initStateCallbacks();
-    }
-
-    private void initStateCallbacks() {
-        mStateCallback = new MultiStateCallback(STATE_NAMES);
-
-        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidated);
-        mStateCallback.addCallback(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidatedWithRecents);
-
-        mStateCallback.addCallback(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
-                this::finishAnimationTargetSetAnimationComplete);
-
-        if (mInQuickSwitchMode) {
-            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
-                            | STATE_RECENTS_PRESENT,
-                    this::finishAnimationTargetSet);
-        } else {
-            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
-                    this::finishAnimationTargetSet);
-        }
-    }
-
-    private void onLauncherAlphaChanged() {
-        if (mRecentsAnimationWrapper.targetSet != null && mEndTarget == null) {
-            applyTransformUnchecked();
-        }
-    }
-
-    @Override
-    protected boolean onActivityInit(final RecentsActivity activity, Boolean alreadyOnHome) {
-        mActivity = activity;
-        mRecentsView = activity.getOverviewPanel();
-        linkRecentsViewScroll();
-        mRecentsView.setDisallowScrollToClearAll(true);
-        mRecentsView.getClearAllButton().setVisibilityAlpha(0);
-
-        mRecentsView.setZoomProgress(1);
-
-        if (!mContinuingLastGesture) {
-            if (mRunningOverHome) {
-                mRecentsView.onGestureAnimationStart(mRunningTaskInfo);
-            } else {
-                mRecentsView.onGestureAnimationStart(mRunningTaskId);
-            }
-        }
-        setStateOnUiThread(STATE_RECENTS_PRESENT);
-        return true;
-    }
-
-    @Override
-    protected boolean moveWindowWithRecentsScroll() {
-        return mInQuickSwitchMode;
-    }
-
-    @Override
-    public void initWhenReady() {
-        if (mInQuickSwitchMode) {
-            // Only init if we are in quickswitch mode
-            super.initWhenReady();
-        }
-    }
-
-    @Override
-    public void updateDisplacement(float displacement) {
-        if (!mInQuickSwitchMode) {
-            super.updateDisplacement(displacement);
-        }
-    }
-
-    @Override
-    protected InputConsumer createNewInputProxyHandler() {
-        // Just consume all input on the active task
-        return InputConsumer.NO_OP;
-    }
-
-    @Override
-    public void onMotionPauseChanged(boolean isPaused) {
-        if (!mInQuickSwitchMode) {
-            mIsMotionPaused = isPaused;
-            mLauncherAlpha.animateToValue(mLauncherAlpha.value, isPaused ? 0 : 1)
-                    .setDuration(150).start();
-            performHapticFeedback();
-        }
-    }
-
-    @Override
-    public Intent getLaunchIntent() {
-        if (mInQuickSwitchMode || mSwipeUpOverHome) {
-            return mOverviewComponentObserver.getOverviewIntent();
-        } else {
-            return mOverviewComponentObserver.getHomeIntent();
-        }
-    }
-
-    @Override
-    public void updateFinalShift() {
-        mTransformParams.setProgress(mCurrentShift.value);
-        mRecentsAnimationWrapper.setWindowThresholdCrossed(!mInQuickSwitchMode
-                && (mCurrentShift.value > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD));
-        if (mRecentsAnimationWrapper.targetSet != null) {
-            applyTransformUnchecked();
-        }
-    }
-
-    @Override
-    public void onGestureCancelled() {
-        updateDisplacement(0);
-        mEndTarget = LAST_TASK;
-        setStateOnUiThread(STATE_GESTURE_CANCELLED);
-    }
-
-    @Override
-    public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
-        mEndVelocityPxPerMs.set(0, velocity.y / 1000);
-        if (mInQuickSwitchMode) {
-            // For now set it to non-null, it will be reset before starting the animation
-            mEndTarget = LAST_TASK;
-        } else {
-            float flingThreshold = mContext.getResources()
-                    .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-            boolean isFling = Math.abs(endVelocity) > flingThreshold;
-
-            if (isFling) {
-                mEndTarget = endVelocity < 0 ? HOME : LAST_TASK;
-            } else if (mIsMotionPaused) {
-                mEndTarget = RECENTS;
-            } else {
-                mEndTarget = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? HOME : LAST_TASK;
-            }
-        }
-        setStateOnUiThread(STATE_GESTURE_COMPLETED);
-    }
-
-    @Override
-    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
-        if (mInQuickSwitchMode && mEndTarget != null) {
-            sharedState.canGestureBeContinued = true;
-            sharedState.goingToLauncher = false;
-
-            mCanceled = true;
-            mCurrentShift.cancelAnimation();
-            if (mFinishAnimation != null) {
-                mFinishAnimation.cancel();
-            }
-
-            if (mRecentsView != null) {
-                if (mFinishingRecentsAnimationForNewTaskId != -1) {
-                    TaskView newRunningTaskView = mRecentsView.getTaskView(
-                            mFinishingRecentsAnimationForNewTaskId);
-                    int newRunningTaskId = newRunningTaskView != null
-                            ? newRunningTaskView.getTask().key.id
-                            : -1;
-                    mRecentsView.setCurrentTask(newRunningTaskId);
-                    sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
-                }
-                mRecentsView.setOnScrollChangeListener(null);
-            }
-        } else {
-            setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-        }
-    }
-
-    private void onHandlerInvalidated() {
-        mActivityInitListener.unregister();
-        if (mGestureEndCallback != null) {
-            mGestureEndCallback.run();
-        }
-        if (mFinishAnimation != null) {
-            mFinishAnimation.end();
-        }
-    }
-
-    private void onHandlerInvalidatedWithRecents() {
-        mRecentsView.onGestureAnimationEnd();
-        mRecentsView.setDisallowScrollToClearAll(false);
-        mRecentsView.getClearAllButton().setVisibilityAlpha(1);
-    }
-
-    private void finishAnimationTargetSetAnimationComplete() {
-        switch (mEndTarget) {
-            case HOME: {
-                if (mSwipeUpOverHome) {
-                    mRecentsAnimationWrapper.finish(false, null, false);
-                    // Send a home intent to clear the task stack
-                    mContext.startActivity(mOverviewComponentObserver.getHomeIntent());
-                } else {
-                    mRecentsAnimationWrapper.finish(true, null, true);
-                }
-                break;
-            }
-            case LAST_TASK:
-                mRecentsAnimationWrapper.finish(false, null, false);
-                break;
-            case RECENTS: {
-                if (mSwipeUpOverHome) {
-                    mRecentsAnimationWrapper.finish(true, null, true);
-                    break;
-                }
-
-                ThumbnailData thumbnail =
-                        mRecentsAnimationWrapper.targetSet.controller.screenshotTask(mRunningTaskId);
-                mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */,
-                        false /* screenshot */);
-
-                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
-                ActivityOptionsCompat.setFreezeRecentTasksList(options);
-
-                Bundle extras = new Bundle();
-                extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
-                extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
-
-                Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
-                        .putExtras(extras);
-                mContext.startActivity(intent, options.toBundle());
-                mRecentsAnimationWrapper.targetSet.controller.cleanupScreenshot();
-                break;
-            }
-            case NEW_TASK: {
-                startNewTask(STATE_HANDLER_INVALIDATED, b -> {});
-                break;
-            }
-        }
-
-        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-    }
-
-    private void finishAnimationTargetSet() {
-        if (mInQuickSwitchMode) {
-            // Recalculate the end target, some views might have been initialized after
-            // gesture has ended.
-            if (mRecentsView == null || !mRecentsAnimationWrapper.hasTargets()) {
-                mEndTarget = LAST_TASK;
-            } else {
-                final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
-                final int taskToLaunch = mRecentsView.getNextPage();
-                mEndTarget = (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
-                        ? NEW_TASK : LAST_TASK;
-            }
-        }
-
-        float endProgress = mEndTarget.mEndProgress;
-        long duration = (long) (mEndTarget.mDurationMultiplier *
-                Math.abs(endProgress - mCurrentShift.value));
-        if (mRecentsView != null) {
-            duration = Math.max(duration, mRecentsView.getScroller().getDuration());
-        }
-        if (mCurrentShift.value != endProgress || mInQuickSwitchMode) {
-            AnimationSuccessListener endListener = new AnimationSuccessListener() {
-
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    finishAnimationTargetSetAnimationComplete();
-                    mFinishAnimation = null;
-                }
-            };
-
-            if (mEndTarget == HOME && !mRunningOverHome) {
-                RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
-                anim.addAnimatorListener(endListener);
-                anim.start(mEndVelocityPxPerMs);
-                mFinishAnimation = RunningWindowAnim.wrap(anim);
-            } else {
-
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(mLauncherAlpha.animateToValue(
-                        mLauncherAlpha.value, mEndTarget.mLauncherAlpha));
-                anim.play(mCurrentShift.animateToValue(mCurrentShift.value, endProgress));
-
-                anim.setDuration(duration);
-                anim.addListener(endListener);
-                anim.start();
-                mFinishAnimation = RunningWindowAnim.wrap(anim);
-            }
-
-        } else {
-            finishAnimationTargetSetAnimationComplete();
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        super.onRecentsAnimationStart(targetSet);
-        mRecentsAnimationWrapper.enableInputConsumer();
-
-        if (mRunningOverHome) {
-            mClipAnimationHelper.prepareAnimation(mDp, true);
-        }
-        applyTransformUnchecked();
-
-        setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled() {
-        mRecentsAnimationWrapper.setController(null);
-        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-    }
-
-    /**
-     * Creates an animation that transforms the current app window into the home app.
-     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
-     */
-    private RectFSpringAnim createWindowAnimationToHome(float startProgress, long duration) {
-        HomeAnimationFactory factory = new HomeAnimationFactory() {
-            @Override
-            public RectF getWindowTargetRect() {
-                return HomeAnimationFactory.getDefaultWindowTargetRect(mDp);
-            }
-
-            @Override
-            public AnimatorPlaybackController createActivityAnimationToHome() {
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(mLauncherAlpha.animateToValue(mLauncherAlpha.value, 1));
-                anim.setDuration(duration);
-                return AnimatorPlaybackController.wrap(anim, duration);
-            }
-        };
-        return createWindowAnimationToHome(startProgress, factory);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index e41880d..26df9c7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -22,45 +22,51 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+import static com.android.launcher3.PagedView.ACTION_MOVE_ALLOW_EASY_FLING;
+import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.util.RaceConditionTracker.ENTER;
-import static com.android.launcher3.util.RaceConditionTracker.EXIT;
-import static com.android.quickstep.TouchInteractionService.INTENT_EXTRA_LOG_TRACE_ID;
-import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
-import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
+import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
+import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
+import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
 import android.graphics.PointF;
-import android.graphics.RectF;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
+
 import androidx.annotation.UiThread;
+
 import com.android.launcher3.R;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.BaseSwipeUpHandler;
 import com.android.quickstep.BaseSwipeUpHandler.Factory;
-import com.android.quickstep.OverviewCallbacks;
-import com.android.quickstep.SwipeSharedState;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
+
 import java.util.function.Consumer;
 
 /**
@@ -73,23 +79,24 @@
     private static final String UP_EVT = "OtherActivityInputConsumer.UP";
 
     // TODO: Move to quickstep contract
-    public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
+    public static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 9;
+    public static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 2;
 
+    private final RecentsAnimationDeviceState mDeviceState;
+    private final NavBarPosition mNavBarPosition;
+    private final TaskAnimationManager mTaskAnimationManager;
+    private final GestureState mGestureState;
+    private RecentsAnimationCallbacks mActiveCallbacks;
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
-    private final RunningTaskInfo mRunningTask;
-    private final OverviewCallbacks mOverviewCallbacks;
-    private final SwipeSharedState mSwipeSharedState;
     private final InputMonitorCompat mInputMonitorCompat;
-    private final SysUINavigationMode.Mode mMode;
-    private final RectF mSwipeTouchRegion;
+    private final BaseActivityInterface mActivityInterface;
 
     private final BaseSwipeUpHandler.Factory mHandlerFactory;
 
-    private final NavBarPosition mNavBarPosition;
-
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
+
     private VelocityTracker mVelocityTracker;
 
     private BaseSwipeUpHandler mInteractionHandler;
@@ -109,6 +116,9 @@
     private boolean mPassedWindowMoveSlop;
     // Slop used to determine when we say that the gesture has started.
     private boolean mPassedPilferInputSlop;
+    // Same as mPassedPilferInputSlop, except when continuing a gesture mPassedPilferInputSlop is
+    // initially true while this one is false.
+    private boolean mPassedSlopOnThisGesture;
 
     // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
     private float mStartDisplacement;
@@ -118,40 +128,38 @@
         ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
                 true /* restoreHomeStackPosition */);
     };
-    private int mLogId;
 
-    public OtherActivityInputConsumer(Context base, RunningTaskInfo runningTaskInfo,
-            boolean isDeferredDownTarget, OverviewCallbacks overviewCallbacks,
-            Consumer<OtherActivityInputConsumer> onCompleteCallback,
-            SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat,
-            RectF swipeTouchRegion, boolean disableHorizontalSwipe,
-            Factory handlerFactory, int logId) {
+    public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState,
+            boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
+            InputMonitorCompat inputMonitorCompat, boolean disableHorizontalSwipe,
+            Factory handlerFactory) {
         super(base);
-        mLogId = logId;
-
+        mDeviceState = deviceState;
+        mNavBarPosition = mDeviceState.getNavBarPosition();
+        mTaskAnimationManager = taskAnimationManager;
+        mGestureState = gestureState;
         mMainThreadHandler = new Handler(Looper.getMainLooper());
-        mRunningTask = runningTaskInfo;
-        mMode = SysUINavigationMode.getMode(base);
-        mSwipeTouchRegion = swipeTouchRegion;
         mHandlerFactory = handlerFactory;
+        mActivityInterface = mGestureState.getActivityInterface();
 
-        mMotionPauseDetector = new MotionPauseDetector(base);
+        mMotionPauseDetector = new MotionPauseDetector(base, false,
+                mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
+                        ? MotionEvent.AXIS_X : MotionEvent.AXIS_Y);
         mMotionPauseMinDisplacement = base.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
         mOnCompleteCallback = onCompleteCallback;
         mVelocityTracker = VelocityTracker.obtain();
         mInputMonitorCompat = inputMonitorCompat;
 
-        boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null;
+        boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
-        mOverviewCallbacks = overviewCallbacks;
-        mSwipeSharedState = swipeSharedState;
 
-        mNavBarPosition = new NavBarPosition(base);
+        float slopMultiplier = mDeviceState.isFullyGesturalNavMode()
+                ? QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL
+                : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
-
-        float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
-        mSquaredTouchSlop = slop * slop;
+        mSquaredTouchSlop = slopMultiplier * mTouchSlop * mTouchSlop;
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
@@ -162,6 +170,11 @@
         return TYPE_OTHER_ACTIVITY;
     }
 
+    @Override
+    public boolean isConsumerDetachedFromGesture() {
+        return true;
+    }
+
     private void forceCancelGesture(MotionEvent ev) {
         int action = ev.getAction();
         ev.setAction(ACTION_CANCEL);
@@ -178,8 +191,12 @@
         // Proxy events to recents view
         if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
-            mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
-                    mNavBarPosition.getRotationMode()));
+            mRecentsViewDispatcher.setConsumer(mInteractionHandler
+                    .getRecentsViewDispatcher(mNavBarPosition.getRotation()));
+            int action = ev.getAction();
+            ev.setAction(ACTION_MOVE_ALLOW_EASY_FLING);
+            mRecentsViewDispatcher.dispatchEvent(ev);
+            ev.setAction(action);
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
@@ -194,8 +211,8 @@
 
         switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
-                RaceConditionTracker.onEvent(DOWN_EVT, ENTER);
-                TraceHelper.beginSection("TouchInt");
+                Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
+                        FLAG_CHECK_FOR_RACE_CONDITIONS);
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
@@ -203,17 +220,17 @@
                 // Start the window animation on down to give more time for launcher to draw if the
                 // user didn't start the gesture over the back button
                 if (!mIsDeferredDownTarget) {
-                    startTouchTrackingForWindowAnimation(ev.getEventTime(), false);
+                    startTouchTrackingForWindowAnimation(ev.getEventTime());
                 }
 
-                RaceConditionTracker.onEvent(DOWN_EVT, EXIT);
+                TraceHelper.INSTANCE.endSection(traceToken);
                 break;
             }
             case ACTION_POINTER_DOWN: {
                 if (!mPassedPilferInputSlop) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         forceCancelGesture(ev);
                     }
                 }
@@ -240,6 +257,7 @@
                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
                 float displacement = getDisplacement(ev);
                 float displacementX = mLastPos.x - mDownPos.x;
+                float displacementY = mLastPos.y - mDownPos.y;
 
                 if (!mPassedWindowMoveSlop) {
                     if (!mIsDeferredDownTarget) {
@@ -254,11 +272,21 @@
 
                 float horizontalDist = Math.abs(displacementX);
                 float upDist = -displacement;
-                boolean isLikelyToStartNewTask = horizontalDist > upDist;
+                boolean passedSlop = squaredHypot(displacementX, displacementY)
+                        >= mSquaredTouchSlop;
+                if (!mPassedSlopOnThisGesture && passedSlop) {
+                    mPassedSlopOnThisGesture = true;
+                }
+                // Until passing slop, we don't know what direction we're going, so assume
+                // we're quick switching to avoid translating recents away when continuing
+                // the gesture (in which case mPassedPilferInputSlop starts as true).
+                boolean haveNotPassedSlopOnContinuedGesture =
+                        !mPassedSlopOnThisGesture && mPassedPilferInputSlop;
+                boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture
+                        || horizontalDist > upDist;
 
                 if (!mPassedPilferInputSlop) {
-                    float displacementY = mLastPos.y - mDownPos.y;
-                    if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+                    if (passedSlop) {
                         if (mDisableHorizontalSwipe
                                 && Math.abs(displacementX) > Math.abs(displacementY)) {
                             // Horizontal gesture is not allowed in this region
@@ -271,15 +299,14 @@
                         if (mIsDeferredDownTarget) {
                             // Deferred gesture, start the animation and gesture tracking once
                             // we pass the actual touch slop
-                            startTouchTrackingForWindowAnimation(
-                                    ev.getEventTime(), isLikelyToStartNewTask);
+                            startTouchTrackingForWindowAnimation(ev.getEventTime());
                         }
                         if (!mPassedWindowMoveSlop) {
                             mPassedWindowMoveSlop = true;
                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
 
                         }
-                        notifyGestureStarted();
+                        notifyGestureStarted(isLikelyToStartNewTask);
                     }
                 }
 
@@ -289,10 +316,10 @@
                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
                     }
 
-                    if (mMode == Mode.NO_BUTTON) {
+                    if (mDeviceState.isFullyGesturalNavMode()) {
                         mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
                                 || isLikelyToStartNewTask);
-                        mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
+                        mMotionPauseDetector.addPosition(ev);
                         mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
                     }
                 }
@@ -300,51 +327,54 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP: {
+                if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
+                    float displacementX = mLastPos.x - mDownPos.x;
+                    float displacementY = mLastPos.y - mDownPos.y;
+                    Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
+                            + " disp=" + squaredHypot(displacementX, displacementY)
+                            + " slop=" + mSquaredTouchSlop);
+                }
                 finishTouchTracking(ev);
                 break;
             }
         }
     }
 
-    private void notifyGestureStarted() {
-        TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+    private void notifyGestureStarted(boolean isLikelyToStartNewTask) {
+        ActiveGestureLog.INSTANCE.addLog("startQuickstep");
         if (mInteractionHandler == null) {
             return;
         }
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitorCompat.pilferPointers();
 
-        mOverviewCallbacks.closeAllWindows();
+        mActivityInterface.closeOverlay();
         ActivityManagerWrapper.getInstance().closeSystemWindows(
                 CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
 
         // Notify the handler that the gesture has actually started
-        mInteractionHandler.onGestureStarted();
+        mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
     }
 
-    private void startTouchTrackingForWindowAnimation(
-            long touchTimeMs, boolean isLikelyToStartNewTask) {
-        TOUCH_INTERACTION_LOG.addLog("startRecentsAnimation");
+    private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
+        ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
 
-        RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener();
-        final BaseSwipeUpHandler handler = mHandlerFactory.newHandler(mRunningTask, touchTimeMs,
-                listenerSet != null, isLikelyToStartNewTask);
+        mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
+                mTaskAnimationManager.isRecentsAnimationRunning());
+        mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
+        mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
+        Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+        mInteractionHandler.initWhenReady(intent);
 
-        mInteractionHandler = handler;
-        handler.setGestureEndCallback(this::onInteractionGestureFinished);
-        mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged);
-        handler.initWhenReady();
-
-        if (listenerSet != null) {
-            listenerSet.addListener(handler);
-            mSwipeSharedState.applyActiveRecentsAnimationState(handler);
-            notifyGestureStarted();
+        if (mTaskAnimationManager.isRecentsAnimationRunning()) {
+            mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
+            mActiveCallbacks.addListener(mInteractionHandler);
+            mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
+            notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
-            RecentsAnimationListenerSet newListenerSet =
-                    mSwipeSharedState.newRecentsAnimationListenerSet();
-            newListenerSet.addListener(handler);
-            Intent intent = handler.getLaunchIntent();
-            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
-            startRecentsActivityAsync(intent, newListenerSet);
+            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
+            mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
+                    mInteractionHandler);
         }
     }
 
@@ -353,8 +383,8 @@
      * the animation can still be running.
      */
     private void finishTouchTracking(MotionEvent ev) {
-        RaceConditionTracker.onEvent(UP_EVT, ENTER);
-        TraceHelper.endSection("TouchInt");
+        Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
+                FLAG_CHECK_FOR_RACE_CONDITIONS);
 
         if (mPassedWindowMoveSlop && mInteractionHandler != null) {
             if (ev.getActionMasked() == ACTION_CANCEL) {
@@ -364,10 +394,11 @@
                         ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
                 float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
                 float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
-                float velocity = mNavBarPosition.isRightEdge() ? velocityX
-                        : mNavBarPosition.isLeftEdge() ? -velocityX
+                float velocity = mNavBarPosition.isRightEdge()
+                        ? velocityX
+                        : mNavBarPosition.isLeftEdge()
+                                ? -velocityX
                                 : velocityY;
-
                 mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
                 mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
                         mDownPos);
@@ -388,7 +419,12 @@
         mVelocityTracker.recycle();
         mVelocityTracker = null;
         mMotionPauseDetector.clear();
-        RaceConditionTracker.onEvent(UP_EVT, EXIT);
+        TraceHelper.INSTANCE.endSection(traceToken);
+    }
+
+    @Override
+    public void notifyOrientationSetup() {
+        mDeviceState.onStartGesture();
     }
 
     @Override
@@ -399,7 +435,7 @@
             // The consumer is being switched while we are active. Set up the shared state to be
             // used by the next animation
             removeListener();
-            mInteractionHandler.onConsumerAboutToBeSwitched(mSwipeSharedState);
+            mInteractionHandler.onConsumerAboutToBeSwitched();
         }
     }
 
@@ -412,9 +448,8 @@
     }
 
     private void removeListener() {
-        RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener();
-        if (listenerSet != null) {
-            listenerSet.removeListener(mInteractionHandler);
+        if (mActiveCallbacks != null) {
+            mActiveCallbacks.removeListener(mInteractionHandler);
         }
     }
 
@@ -429,12 +464,7 @@
     }
 
     @Override
-    public boolean useSharedSwipeState() {
-        return mInteractionHandler != null;
-    }
-
-    @Override
     public boolean allowInterceptByParent() {
-        return !mPassedPilferInputSlop;
+        return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
new file mode 100644
index 0000000..fb420a2
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_POINTER_UP;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import static java.lang.Math.abs;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.OverscrollPlugin;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
+ */
+public class OverscrollInputConsumer extends DelegateInputConsumer {
+    private static final String TAG = "OverscrollInputConsumer";
+    private static final boolean DEBUG_LOGS_ENABLED = false;
+    private static void debugPrint(String log) {
+        if (DEBUG_LOGS_ENABLED) {
+            Log.v(TAG, log);
+        }
+    }
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private final PointF mStartDragPos = new PointF();
+    private final int mAngleThreshold;
+
+    private final int mFlingDistanceThresholdPx;
+    private final int mFlingVelocityThresholdPx;
+    private int mActivePointerId = -1;
+    private boolean mPassedSlop = false;
+    // True if we set ourselves as active, meaning we no longer pass events to the delegate.
+    private boolean mPassedActiveThreshold = false;
+    // When a gesture crosses this length, this recognizer will attempt to interpret touch events.
+    private final float mSquaredSlop;
+    // When a gesture crosses this length, this recognizer will become the sole active recognizer.
+    private final float mSquaredActiveThreshold;
+    // When a gesture crosses this length, the overscroll view should be shown.
+    private final float mSquaredFinishThreshold;
+    private boolean mThisDownIsIgnored = false;
+
+    private final GestureState mGestureState;
+    @Nullable
+    private final OverscrollPlugin mPlugin;
+
+    @Nullable
+    private RecentsView mRecentsView;
+
+    public OverscrollInputConsumer(Context context, GestureState gestureState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
+        super(delegate, inputMonitor);
+
+        mAngleThreshold = context.getResources()
+                .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+        mFlingDistanceThresholdPx = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+        mFlingVelocityThresholdPx = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+        mGestureState = gestureState;
+        mPlugin = plugin;
+
+        float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+        mSquaredSlop = slop * slop;
+
+
+        float finishGestureThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_finish_threshold);
+        mSquaredFinishThreshold = finishGestureThreshold * finishGestureThreshold;
+
+        float activeThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_active_threshold);
+        mSquaredActiveThreshold = activeThreshold * activeThreshold;
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_OVERSCROLL | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        if (mPlugin == null) {
+            return;
+        }
+
+        debugPrint("got event, underlying activity is " + getUnderlyingActivity());
+        switch (ev.getActionMasked()) {
+            case ACTION_DOWN: {
+                debugPrint("ACTION_DOWN");
+                mActivePointerId = ev.getPointerId(0);
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                if (mPlugin.blockOtherGestures()) {
+                    debugPrint("mPlugin.blockOtherGestures(), becoming active on ACTION_DOWN");
+                    // Otherwise, if an appear gesture is performed when the Activity is visible,
+                    // the Activity will dismiss its keyboard.
+                    mPassedActiveThreshold = true;
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    setActive(ev);
+                }
+                break;
+            }
+            case ACTION_POINTER_DOWN: {
+                if (mState != STATE_ACTIVE) {
+                    mState = STATE_DELEGATE_ACTIVE;
+                }
+                break;
+            }
+            case ACTION_POINTER_UP: {
+                int ptrIdx = ev.getActionIndex();
+                int ptrId = ev.getPointerId(ptrIdx);
+                if (ptrId == mActivePointerId) {
+                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                    mDownPos.set(
+                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                    mActivePointerId = ev.getPointerId(newPointerIdx);
+                }
+                break;
+            }
+            case ACTION_MOVE: {
+                if (mState == STATE_DELEGATE_ACTIVE) {
+                    break;
+                }
+
+                if (!mDelegate.allowInterceptByParent()) {
+                    mState = STATE_DELEGATE_ACTIVE;
+                    break;
+                }
+
+                // Update last touch position.
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == -1) {
+                    break;
+                }
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+                if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) {
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
+                }
+
+                boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled()
+                        && (squaredDist > mSquaredActiveThreshold);
+                if (becomeActive) {
+                    debugPrint("Past slop and past threshold, set active");
+                    mPassedActiveThreshold = true;
+                    setActive(ev);
+                }
+
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_MOVE Relaying touch event");
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
+                }
+
+                break;
+            }
+            case ACTION_CANCEL:
+            case ACTION_UP:
+                debugPrint("ACTION_UP");
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_UP Relaying touch event");
+
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
+                }
+
+
+                mPassedSlop = false;
+                mPassedActiveThreshold = false;
+                mState = STATE_INACTIVE;
+                break;
+        }
+
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    private boolean isOverscrolled() {
+        if (mRecentsView == null) {
+            BaseDraggingActivity activity = mGestureState.getActivityInterface()
+                    .getCreatedActivity();
+            if (activity != null) {
+                mRecentsView = activity.getOverviewPanel();
+            }
+        }
+
+        // Make sure there isn't an app to quick switch to on our right
+        int maxIndex = 0;
+        if (mRecentsView != null && mRecentsView.hasRecentsExtraCard()) {
+            maxIndex = 1;
+        }
+
+        boolean atRightMostApp = mRecentsView == null
+                || (mRecentsView.getRunningTaskIndex() <= maxIndex);
+
+        // Check if the gesture is within our angle threshold of horizontal
+        float deltaY = abs(mLastPos.y - mDownPos.y);
+        float deltaX = mLastPos.x - mDownPos.x;
+
+        boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, abs(deltaX))) < mAngleThreshold);
+
+        boolean overscrollVisible = mPlugin.blockOtherGestures();
+        boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0;
+        boolean gestureDirectionMatchesVisibility = overscrollVisible
+                || overscrollInvisibleAndLeftSwipe;
+        return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility;
+    }
+
+    private String getDeviceState() {
+        String deviceState = OverscrollPlugin.DEVICE_STATE_UNKNOWN;
+        int consumerType = mDelegate.getType();
+        if (((consumerType & InputConsumer.TYPE_OVERVIEW) > 0)
+                || ((consumerType & InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS)) > 0) {
+            deviceState = OverscrollPlugin.DEVICE_STATE_LAUNCHER;
+        } else if ((consumerType & InputConsumer.TYPE_OTHER_ACTIVITY) > 0) {
+            deviceState = OverscrollPlugin.DEVICE_STATE_APP;
+        } else if (((consumerType & InputConsumer.TYPE_RESET_GESTURE) > 0)
+                || ((consumerType & InputConsumer.TYPE_DEVICE_LOCKED) > 0)) {
+            deviceState = OverscrollPlugin.DEVICE_STATE_LOCKED;
+        }
+
+        return deviceState;
+    }
+
+    private int getHorizontalDistancePx() {
+        return (int) (mLastPos.x - mDownPos.x);
+    }
+
+    private int getVerticalDistancePx() {
+        return (int) (mLastPos.y - mDownPos.y);
+    }
+
+    private @NonNull String getUnderlyingActivity() {
+        // Overly defensive, got guidance on code review that something in the chain of
+        // `mGestureState.getRunningTask().topActivity` can be null and thus cause a null pointer
+        // exception to be thrown, but we aren't sure which part can be null.
+        if ((mGestureState == null) || (mGestureState.getRunningTask() == null)
+                || (mGestureState.getRunningTask().topActivity == null)) {
+            return "";
+        }
+        return mGestureState.getRunningTask().topActivity.flattenToString();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index 581ab6d..9bfe84f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -16,7 +16,6 @@
 package com.android.quickstep.inputconsumers;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.util.Log;
@@ -25,49 +24,43 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.OverviewCallbacks;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
-import java.util.function.Predicate;
-
 /**
  * Input consumer for handling touch on the recents/Launcher activity.
  */
-public class OverviewInputConsumer<T extends BaseDraggingActivity>
+public class OverviewInputConsumer<T extends StatefulActivity<?>>
         implements InputConsumer {
 
     private final T mActivity;
+    private final BaseActivityInterface<?, T> mActivityInterface;
     private final BaseDragLayer mTarget;
     private final InputMonitorCompat mInputMonitor;
 
     private final int[] mLocationOnScreen = new int[2];
-    private final boolean mProxyTouch;
-    private final Predicate<MotionEvent> mEventReceiver;
 
     private final boolean mStartingInActivityBounds;
     private boolean mTargetHandledTouch;
 
-    public OverviewInputConsumer(T activity, @Nullable InputMonitorCompat inputMonitor,
-            boolean startingInActivityBounds) {
+    public OverviewInputConsumer(GestureState gestureState, T activity,
+            @Nullable InputMonitorCompat inputMonitor, boolean startingInActivityBounds) {
         mActivity = activity;
         mInputMonitor = inputMonitor;
         mStartingInActivityBounds = startingInActivityBounds;
+        mActivityInterface = gestureState.getActivityInterface();
 
         mTarget = activity.getDragLayer();
-        if (startingInActivityBounds) {
-            mEventReceiver = mTarget::dispatchTouchEvent;
-            mProxyTouch = true;
-        } else {
-            // Only proxy touches to controllers if we are starting touch from nav bar.
-            mEventReceiver = mTarget::proxyTouchEvent;
-            mTarget.getLocationOnScreen(mLocationOnScreen);
-            mProxyTouch = mTarget.prepareProxyEventStarting();
-        }
+        mTarget.getLocationOnScreen(mLocationOnScreen);
     }
 
     @Override
@@ -82,28 +75,28 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
-        if (!mProxyTouch) {
-            return;
-        }
-
         int flags = ev.getEdgeFlags();
         if (!mStartingInActivityBounds) {
             ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
         }
         ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
-        boolean handled = mEventReceiver.test(ev);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "OverviewInputConsumer");
+        }
+        boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
         ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
         ev.setEdgeFlags(flags);
 
         if (!mTargetHandledTouch && handled) {
             mTargetHandledTouch = true;
             if (!mStartingInActivityBounds) {
-                OverviewCallbacks.get(mActivity).closeAllWindows();
+                mActivityInterface.closeOverlay();
                 ActivityManagerWrapper.getInstance()
                         .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+                ActiveGestureLog.INSTANCE.addLog("startQuickstep");
             }
             if (mInputMonitor != null) {
+                TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
                 mInputMonitor.pilferPointers();
             }
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index f40d552..1c77a05 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -15,52 +15,44 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_UP;
-
-import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
 
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.PointF;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.logging.StatsLogUtils;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
-public class OverviewWithoutFocusInputConsumer implements InputConsumer {
+public class OverviewWithoutFocusInputConsumer implements InputConsumer,
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
 
-    private final InputMonitorCompat mInputMonitor;
-    private final boolean mDisableHorizontalSwipe;
-    private final PointF mDownPos = new PointF();
-    private final float mSquaredTouchSlop;
     private final Context mContext;
-    private final NavBarPosition mNavBarPosition;
+    private final InputMonitorCompat mInputMonitor;
+    private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+    private final GestureState mGestureState;
 
-    private boolean mInterceptedTouch;
-    private VelocityTracker mVelocityTracker;
-
-
-    public OverviewWithoutFocusInputConsumer(Context context, InputMonitorCompat inputMonitor,
-            boolean disableHorizontalSwipe) {
-        mInputMonitor = inputMonitor;
-        mDisableHorizontalSwipe = disableHorizontalSwipe;
+    public OverviewWithoutFocusInputConsumer(Context context,
+            RecentsAnimationDeviceState deviceState, GestureState gestureState,
+            InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
         mContext = context;
-        mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
-        mNavBarPosition = new NavBarPosition(context);
-
-        mVelocityTracker = VelocityTracker.obtain();
+        mGestureState = gestureState;
+        mInputMonitor = inputMonitor;
+        mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
+                deviceState.getNavBarPosition(), this::onInterceptTouch, this);
     }
 
     @Override
@@ -70,94 +62,45 @@
 
     @Override
     public boolean allowInterceptByParent() {
-        return !mInterceptedTouch;
-    }
-
-    private void endTouchTracking() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
-            mVelocityTracker = null;
-        }
+        return !mTriggerSwipeUpTracker.interceptedTouch();
     }
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
-        if (mVelocityTracker == null) {
-            return;
-        }
+        mTriggerSwipeUpTracker.onMotionEvent(ev);
+    }
 
-        mVelocityTracker.addMovement(ev);
-        switch (ev.getActionMasked()) {
-            case ACTION_DOWN: {
-                mDownPos.set(ev.getX(), ev.getY());
-                break;
-            }
-            case ACTION_MOVE: {
-                if (!mInterceptedTouch) {
-                    float displacementX = ev.getX() - mDownPos.x;
-                    float displacementY = ev.getY() - mDownPos.y;
-                    if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
-                        if (mDisableHorizontalSwipe
-                                && Math.abs(displacementX) > Math.abs(displacementY)) {
-                            // Horizontal gesture is not allowed in this region
-                            endTouchTracking();
-                            break;
-                        }
-
-                        mInterceptedTouch = true;
-
-                        if (mInputMonitor != null) {
-                            mInputMonitor.pilferPointers();
-                        }
-                    }
-                }
-                break;
-            }
-
-            case ACTION_CANCEL:
-                endTouchTracking();
-                break;
-
-            case ACTION_UP: {
-                finishTouchTracking(ev);
-                endTouchTracking();
-                break;
-            }
+    private void onInterceptTouch() {
+        if (mInputMonitor != null) {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+            mInputMonitor.pilferPointers();
         }
     }
 
-    private void finishTouchTracking(MotionEvent ev) {
-        mVelocityTracker.computeCurrentVelocity(100);
-        float velocityX = mVelocityTracker.getXVelocity();
-        float velocityY = mVelocityTracker.getYVelocity();
-        float velocity = mNavBarPosition.isRightEdge()
-                ? -velocityX : (mNavBarPosition.isLeftEdge() ? velocityX : -velocityY);
-
-        final boolean triggerQuickstep;
-        int touch = Touch.FLING;
-        if (Math.abs(velocity) >= ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) {
-            triggerQuickstep = velocity > 0;
-        } else {
-            float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
-            float displacementY = ev.getY() - mDownPos.y;
-            triggerQuickstep = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
-            touch = Touch.SWIPE;
-        }
-
-        if (triggerQuickstep) {
-            mContext.startActivity(new Intent(Intent.ACTION_MAIN)
-                    .addCategory(Intent.CATEGORY_HOME)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-            TOUCH_INTERACTION_LOG.addLog("startQuickstep");
-            BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
-            int pageIndex = -1; // This number doesn't reflect workspace page index.
-                                // It only indicates that launcher client screen was shown.
-            int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
-            activity.getUserEventDispatcher().logActionOnContainer(
-                    touch, Direction.UP, containerType, pageIndex);
-            activity.getUserEventDispatcher().setPreviousHomeGesture(true);
-        } else {
-            // ignore
-        }
+    @Override
+    public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+        mContext.startActivity(mGestureState.getHomeIntent());
+        ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+        BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+        int pageIndex = -1; // This number doesn't reflect workspace page index.
+                            // It only indicates that launcher client screen was shown.
+        int containerType = (mGestureState != null && mGestureState.getEndTarget() != null)
+                ? mGestureState.getEndTarget().containerType
+                : LauncherLogProto.ContainerType.WORKSPACE;
+        activity.getUserEventDispatcher().logActionOnContainer(
+                wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
+        activity.getUserEventDispatcher().setPreviousHomeGesture(true);
+        activity.getStatsLogManager().logger()
+                .withSrcState(LAUNCHER_STATE_HOME)
+                .withDstState(LAUNCHER_STATE_HOME)
+                .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                        .setWorkspace(
+                                LauncherAtom.WorkspaceContainer.newBuilder()
+                                        .setPageIndex(-1))
+                        .build())
+                .log(LAUNCHER_HOME_GESTURE);
     }
+
+    @Override
+    public void onSwipeUpCancelled() {}
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
index 8eede81..d34b40b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
@@ -17,17 +17,18 @@
 
 import android.view.MotionEvent;
 
-import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.TaskAnimationManager;
 
 /**
  * A NO_OP input consumer which also resets any pending gesture
  */
 public class ResetGestureInputConsumer implements InputConsumer {
 
-    private final SwipeSharedState mSwipeSharedState;
+    private final TaskAnimationManager mTaskAnimationManager;
 
-    public ResetGestureInputConsumer(SwipeSharedState swipeSharedState) {
-        mSwipeSharedState = swipeSharedState;
+    public ResetGestureInputConsumer(TaskAnimationManager taskAnimationManager) {
+        mTaskAnimationManager = taskAnimationManager;
     }
 
     @Override
@@ -38,8 +39,8 @@
     @Override
     public void onMotionEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN
-                && mSwipeSharedState.getActiveListener() != null) {
-            mSwipeSharedState.clearAllState(false /* finishAnimation */);
+                && mTaskAnimationManager.isRecentsAnimationRunning()) {
+            mTaskAnimationManager.finishRunningRecentsAnimation(false /* toHome */);
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
index a0e20f2..b9827ff 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
@@ -16,16 +16,15 @@
 package com.android.quickstep.inputconsumers;
 
 import android.content.Context;
-import android.os.RemoteException;
-import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.quickstep.ActivityControlHelper;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.MotionPauseDetector;
-import com.android.systemui.shared.recents.ISystemUiProxy;
 
 /**
  * An input consumer that detects swipe up and hold to exit screen pinning mode.
@@ -39,25 +38,21 @@
 
     private float mTouchDownY;
 
-    public ScreenPinnedInputConsumer(Context context, ISystemUiProxy sysuiProxy,
-            ActivityControlHelper activityControl) {
+    public ScreenPinnedInputConsumer(Context context, GestureState gestureState) {
         mMotionPauseMinDisplacement = context.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
         mMotionPauseDetector = new MotionPauseDetector(context, true /* makePauseHarderToTrigger*/);
         mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
             if (isPaused) {
-                try {
-                    sysuiProxy.stopScreenPinning();
-                    BaseDraggingActivity launcherActivity = activityControl.getCreatedActivity();
-                    if (launcherActivity != null) {
-                        launcherActivity.getRootView().performHapticFeedback(
-                                HapticFeedbackConstants.LONG_PRESS,
-                                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
-                    }
-                    mMotionPauseDetector.clear();
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Unable to stop screen pinning ", e);
+                SystemUiProxy.INSTANCE.get(context).stopScreenPinning();
+                BaseDraggingActivity launcherActivity = gestureState.getActivityInterface()
+                        .getCreatedActivity();
+                if (launcherActivity != null) {
+                    launcherActivity.getRootView().performHapticFeedback(
+                            HapticFeedbackConstants.LONG_PRESS,
+                            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                 }
+                mMotionPauseDetector.clear();
             }
         });
     }
@@ -77,7 +72,7 @@
             case MotionEvent.ACTION_MOVE:
                 float displacement = mTouchDownY - y;
                 mMotionPauseDetector.setDisallowPause(displacement < mMotionPauseMinDisplacement);
-                mMotionPauseDetector.addPosition(y, ev.getEventTime());
+                mMotionPauseDetector.addPosition(ev);
                 break;
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java
new file mode 100644
index 0000000..3f833c0
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Input consumer used when a fullscreen System UI overlay is showing (such as the expanded Bubbles
+ * UI).
+ *
+ * This responds to swipes up by sending a closeSystemDialogs broadcast (causing overlays to close)
+ * rather than closing the app behind the overlay and sending the user all the way home.
+ */
+public class SysUiOverlayInputConsumer implements InputConsumer,
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+
+    private final Context mContext;
+    private final InputMonitorCompat mInputMonitor;
+    private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+
+    public SysUiOverlayInputConsumer(
+            Context context,
+            RecentsAnimationDeviceState deviceState,
+            InputMonitorCompat inputMonitor) {
+        mContext = context;
+        mInputMonitor = inputMonitor;
+        mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, true,
+                deviceState.getNavBarPosition(), this::onInterceptTouch, this);
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_SYSUI_OVERLAY;
+    }
+
+    @Override
+    public boolean allowInterceptByParent() {
+        return !mTriggerSwipeUpTracker.interceptedTouch();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        mTriggerSwipeUpTracker.onMotionEvent(ev);
+    }
+
+    private void onInterceptTouch() {
+        if (mInputMonitor != null) {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+            mInputMonitor.pilferPointers();
+        }
+    }
+
+    @Override
+    public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+        // Close system dialogs when a swipe up is detected.
+        mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+    }
+
+    @Override
+    public void onSwipeUpCancelled() {
+
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
index b251f9e..e000803 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
@@ -19,10 +19,12 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 
+import java.util.ArrayList;
+
 /**
  * This class handles AOSP MetricsLogger function calls and logging around
  * quickstep interactions and app launches.
@@ -41,7 +43,7 @@
     @Override
     protected void onFillInLogContainerData(
             @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target,
-            @NonNull LauncherLogProto.Target targetParent) {
+            @NonNull ArrayList<LauncherLogProto.Target> targets) {
         PredictionUiStateManager.fillInPredictedRank(itemInfo, target);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
new file mode 100644
index 0000000..fabfc4b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.content.Context;
+
+import com.android.launcher3.logging.EventLogArray;
+import com.android.launcher3.util.MainThreadInitializedObject;
+
+/**
+ * A log to keep track of the active gesture.
+ */
+public class ActiveGestureLog extends EventLogArray {
+
+    public static final ActiveGestureLog INSTANCE = new ActiveGestureLog();
+
+    /**
+     * NOTE: This value should be kept same as
+     * ActivityTaskManagerService#INTENT_EXTRA_LOG_TRACE_ID in platform
+     */
+    public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID";
+
+    private ActiveGestureLog() {
+        super("touch_interaction_log", 40);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
similarity index 100%
rename from quickstep/src/com/android/quickstep/util/AssistantUtilities.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
deleted file mode 100644
index 68007da..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ /dev/null
@@ -1,442 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
-import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Build;
-import android.os.RemoteException;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.utilities.RectFEvaluator;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-import com.android.systemui.shared.system.TransactionCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-/**
- * Utility class to handle window clip animation
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class ClipAnimationHelper {
-
-    // The bounds of the source app in device coordinates
-    private final Rect mSourceStackBounds = new Rect();
-    // The insets of the source app
-    private final Rect mSourceInsets = new Rect();
-    // The source app bounds with the source insets applied, in the source app window coordinates
-    private final RectF mSourceRect = new RectF();
-    // The bounds of the task view in launcher window coordinates
-    private final RectF mTargetRect = new RectF();
-    // The insets to be used for clipping the app window, which can be larger than mSourceInsets
-    // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
-    // app window coordinates.
-    private final RectF mSourceWindowClipInsets = new RectF();
-    // The insets to be used for clipping the app window. For live tile, we don't transform the clip
-    // relative to the target rect.
-    private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
-
-    // The bounds of launcher (not including insets) in device coordinates
-    public final Rect mHomeStackBounds = new Rect();
-
-    // The clip rect in source app window coordinates
-    private final RectF mClipRectF = new RectF();
-    private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
-    private final Matrix mTmpMatrix = new Matrix();
-    private final Rect mTmpRect = new Rect();
-    private final RectF mTmpRectF = new RectF();
-    private final RectF mCurrentRectWithInsets = new RectF();
-    // Corner radius of windows, in pixels
-    private final float mWindowCornerRadius;
-    // Corner radius of windows when they're in overview mode.
-    private final float mTaskCornerRadius;
-    // If windows can have real time rounded corners.
-    private final boolean mSupportsRoundedCornersOnWindows;
-    // Whether or not to actually use the rounded cornders on windows
-    private boolean mUseRoundedCornersOnWindows;
-
-    // Corner radius currently applied to transformed window.
-    private float mCurrentCornerRadius;
-
-    // Whether to boost the opening animation target layers, or the closing
-    private int mBoostModeTargetLayers = -1;
-
-    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
-    private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;
-
-    public ClipAnimationHelper(Context context) {
-        mWindowCornerRadius = getWindowCornerRadius(context.getResources());
-        mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
-        mTaskCornerRadius = TaskCornerRadius.get(context);
-        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
-    }
-
-    private void updateSourceStack(RemoteAnimationTargetCompat target) {
-        mSourceInsets.set(target.contentInsets);
-        mSourceStackBounds.set(target.sourceContainerBounds);
-
-        // TODO: Should sourceContainerBounds already have this offset?
-        mSourceStackBounds.offsetTo(target.position.x, target.position.y);
-
-    }
-
-    public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
-        updateSourceStack(target);
-        updateHomeBounds(homeStackBounds);
-    }
-
-    public void updateHomeBounds(Rect homeStackBounds) {
-        mHomeStackBounds.set(homeStackBounds);
-    }
-
-    public void updateTargetRect(Rect targetRect) {
-        mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
-                mSourceStackBounds.width() - mSourceInsets.right,
-                mSourceStackBounds.height() - mSourceInsets.bottom);
-        mTargetRect.set(targetRect);
-        mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
-                mHomeStackBounds.top - mSourceStackBounds.top);
-
-        // Calculate the clip based on the target rect (since the content insets and the
-        // launcher insets may differ, so the aspect ratio of the target rect can differ
-        // from the source rect. The difference between the target rect (scaled to the
-        // source rect) is the amount to clip on each edge.
-        RectF scaledTargetRect = new RectF(mTargetRect);
-        Utilities.scaleRectFAboutCenter(scaledTargetRect,
-                mSourceRect.width() / mTargetRect.width());
-        scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
-        mSourceWindowClipInsets.set(
-                Math.max(scaledTargetRect.left, 0),
-                Math.max(scaledTargetRect.top, 0),
-                Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
-                Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
-        mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
-        mSourceRect.set(scaledTargetRect);
-    }
-
-    public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
-        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
-        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
-    }
-
-    public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
-        return applyTransform(targetSet, params, true /* launcherOnTop */);
-    }
-
-    public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params,
-            boolean launcherOnTop) {
-        float progress = params.progress;
-        if (params.currentRect == null) {
-            RectF currentRect;
-            mTmpRectF.set(mTargetRect);
-            Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
-            currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
-            currentRect.offset(params.offsetX, 0);
-
-            // Don't clip past progress > 1.
-            progress = Math.min(1, progress);
-            final RectF sourceWindowClipInsets = params.forLiveTile
-                    ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
-            mClipRectF.left = sourceWindowClipInsets.left * progress;
-            mClipRectF.top = sourceWindowClipInsets.top * progress;
-            mClipRectF.right =
-                    mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
-            mClipRectF.bottom =
-                    mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
-            params.setCurrentRectAndTargetAlpha(currentRect, 1);
-        }
-
-        SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
-        for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
-            RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
-            mTmpMatrix.setTranslate(app.position.x, app.position.y);
-            Rect crop = mTmpRect;
-            crop.set(app.sourceContainerBounds);
-            crop.offsetTo(0, 0);
-            float alpha;
-            int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
-            float cornerRadius = 0f;
-            float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
-            if (app.mode == targetSet.targetMode) {
-                alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
-                if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                    mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
-                    mTmpMatrix.postTranslate(app.position.x, app.position.y);
-                    mClipRectF.roundOut(crop);
-                    if (mSupportsRoundedCornersOnWindows) {
-                        if (params.cornerRadius > -1) {
-                            cornerRadius = params.cornerRadius;
-                            scale = params.currentRect.width() / crop.width();
-                        } else {
-                            float windowCornerRadius = mUseRoundedCornersOnWindows
-                                    ? mWindowCornerRadius : 0;
-                            cornerRadius = Utilities.mapRange(progress, windowCornerRadius,
-                                    mTaskCornerRadius);
-                        }
-                        mCurrentCornerRadius = cornerRadius;
-                    }
-                    // Fade out Assistant overlay.
-                    if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
-                            && app.isNotInRecents) {
-                        alpha = 1 - Interpolators.DEACCEL_2_5.getInterpolation(progress);
-                    }
-                } else if (targetSet.hasRecents) {
-                    // If home has a different target then recents, reverse anim the
-                    // home target.
-                    alpha = 1 - (progress * params.targetAlpha);
-                }
-            } else {
-                alpha = mBaseAlphaCallback.getAlpha(app, progress);
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
-                    crop = null;
-                    layer = Integer.MAX_VALUE;
-                }
-            }
-
-            // Since radius is in Surface space, but we draw the rounded corners in screen space, we
-            // have to undo the scale.
-            surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
-                    cornerRadius / scale);
-        }
-        applySurfaceParams(params.syncTransactionApplier, surfaceParams);
-        return params.currentRect;
-    }
-
-    public RectF getCurrentRectWithInsets() {
-        mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
-        return mCurrentRectWithInsets;
-    }
-
-    private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat
-            syncTransactionApplier, SurfaceParams[] params) {
-        if (syncTransactionApplier != null) {
-            syncTransactionApplier.scheduleApply(params);
-        } else {
-            TransactionCompat t = new TransactionCompat();
-            for (SurfaceParams param : params) {
-                SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
-            }
-            t.setEarlyWakeup();
-            t.apply();
-        }
-    }
-
-    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
-        mTaskAlphaCallback = callback;
-    }
-
-    public void setBaseAlphaCallback(TargetAlphaProvider callback) {
-        mBaseAlphaCallback = callback;
-    }
-
-    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
-        fromTaskThumbnailView(ttv, rv, null);
-    }
-
-    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
-            @Nullable RemoteAnimationTargetCompat target) {
-        BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
-        BaseDragLayer dl = activity.getDragLayer();
-
-        int[] pos = new int[2];
-        dl.getLocationOnScreen(pos);
-        mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
-        mHomeStackBounds.offset(pos[0], pos[1]);
-
-        if (target != null) {
-            updateSourceStack(target);
-        } else  if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
-            updateStackBoundsToMultiWindowTaskSize(activity);
-        } else {
-            mSourceStackBounds.set(mHomeStackBounds);
-            Rect fallback = dl.getInsets();
-            mSourceInsets.set(ttv.getInsets(fallback));
-        }
-
-        Rect targetRect = new Rect();
-        dl.getDescendantRectRelativeToSelf(ttv, targetRect);
-        updateTargetRect(targetRect);
-
-        if (target == null) {
-            // Transform the clip relative to the target rect. Only do this in the case where we
-            // aren't applying the insets to the app windows (where the clip should be in target app
-            // space)
-            float scale = mTargetRect.width() / mSourceRect.width();
-            mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
-            mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
-            mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
-            mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
-        }
-    }
-
-    /**
-     * Compute scale and translation y such that the specified task view fills the screen.
-     */
-    public ClipAnimationHelper updateForFullscreenOverview(TaskView v) {
-        TaskThumbnailView thumbnailView = v.getThumbnail();
-        RecentsView recentsView = v.getRecentsView();
-        fromTaskThumbnailView(thumbnailView, recentsView);
-        Rect taskSize = new Rect();
-        recentsView.getTaskSize(taskSize);
-        updateTargetRect(taskSize);
-        return this;
-    }
-
-    /**
-     * @return The source rect's scale and translation relative to the target rect.
-     */
-    public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
-        float scale = mSourceRect.width() / mTargetRect.width();
-        float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
-        return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
-    }
-
-    private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
-        ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
-        if (sysUiProxy != null) {
-            try {
-                mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
-                return;
-            } catch (RemoteException e) {
-                // Use half screen size
-            }
-        }
-
-        // Assume that the task size is half screen size (minus the insets and the divider size)
-        DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
-        // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
-        // account for system insets
-        int taskWidth = fullDp.availableWidthPx;
-        int taskHeight = fullDp.availableHeightPx;
-        int halfDividerSize = activity.getResources()
-                .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
-
-        Rect insets = new Rect();
-        WindowManagerWrapper.getInstance().getStableInsets(insets);
-        if (fullDp.isLandscape) {
-            taskWidth = taskWidth / 2 - halfDividerSize;
-        } else {
-            taskHeight = taskHeight / 2 - halfDividerSize;
-        }
-
-        // Align the task to bottom left/right edge (closer to nav bar).
-        int left = activity.getDeviceProfile().isSeascape() ? insets.left
-                : (insets.left + fullDp.availableWidthPx - taskWidth);
-        mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
-        mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
-    }
-
-    public RectF getTargetRect() {
-        return mTargetRect;
-    }
-
-    public float getCurrentCornerRadius() {
-        return mCurrentCornerRadius;
-    }
-
-    public interface TargetAlphaProvider {
-        float getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha);
-    }
-
-    public static class TransformParams {
-        float progress;
-        public float offsetX;
-        public float offsetScale;
-        @Nullable RectF currentRect;
-        float targetAlpha;
-        boolean forLiveTile;
-        float cornerRadius;
-
-        SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
-
-        public TransformParams() {
-            progress = 0;
-            offsetX = 0;
-            offsetScale = 1;
-            currentRect = null;
-            targetAlpha = 0;
-            forLiveTile = false;
-            cornerRadius = -1;
-        }
-
-        public TransformParams setProgress(float progress) {
-            this.progress = progress;
-            this.currentRect = null;
-            return this;
-        }
-
-        public float getProgress() {
-            return progress;
-        }
-
-        public TransformParams setCornerRadius(float cornerRadius) {
-            this.cornerRadius = cornerRadius;
-            return this;
-        }
-
-        public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
-            this.currentRect = currentRect;
-            this.targetAlpha = targetAlpha;
-            return this;
-        }
-
-        public TransformParams setOffsetX(float offsetX) {
-            this.offsetX = offsetX;
-            return this;
-        }
-
-        public TransformParams setOffsetScale(float offsetScale) {
-            this.offsetScale = offsetScale;
-            return this;
-        }
-
-        public TransformParams setForLiveTile(boolean forLiveTile) {
-            this.forLiveTile = forLiveTile;
-            return this;
-        }
-
-        public TransformParams setSyncTransactionApplier(
-                SyncRtSurfaceTransactionApplierCompat applier) {
-            this.syncTransactionApplier = applier;
-            return this;
-        }
-    }
-}
-
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
new file mode 100644
index 0000000..190763a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H;
+import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import com.android.launcher3.tracing.nano.LauncherTraceProto;
+import com.android.launcher3.tracing.nano.LauncherTraceEntryProto;
+import com.android.launcher3.tracing.nano.LauncherTraceFileProto;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.systemui.shared.tracing.FrameProtoTracer;
+import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
+import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Queue;
+
+
+/**
+ * Controller for coordinating winscope proto tracing.
+ */
+public class ProtoTracer implements ProtoTraceParams<MessageNano,
+        LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> {
+
+    public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
+            new MainThreadInitializedObject<>(ProtoTracer::new);
+
+    private static final String TAG = "ProtoTracer";
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final Context mContext;
+    private final FrameProtoTracer<MessageNano,
+            LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> mProtoTracer;
+
+    public ProtoTracer(Context context) {
+        mContext = context;
+        mProtoTracer = new FrameProtoTracer<>(this);
+    }
+
+    @Override
+    public File getTraceFile() {
+        return new File(mContext.getFilesDir(), "launcher_trace.pb");
+    }
+
+    @Override
+    public LauncherTraceFileProto getEncapsulatingTraceProto() {
+        return new LauncherTraceFileProto();
+    }
+
+    @Override
+    public LauncherTraceEntryProto updateBufferProto(LauncherTraceEntryProto reuseObj,
+            ArrayList<ProtoTraceable<LauncherTraceProto>> traceables) {
+        LauncherTraceEntryProto proto = reuseObj != null
+                ? reuseObj
+                : new LauncherTraceEntryProto();
+        proto.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+        proto.launcher = proto.launcher != null ? proto.launcher : new LauncherTraceProto();
+        for (ProtoTraceable t : traceables) {
+            t.writeToProto(proto.launcher);
+        }
+        return proto;
+    }
+
+    @Override
+    public byte[] serializeEncapsulatingProto(LauncherTraceFileProto encapsulatingProto,
+            Queue<LauncherTraceEntryProto> buffer) {
+        encapsulatingProto.magicNumber = MAGIC_NUMBER_VALUE;
+        encapsulatingProto.entry = buffer.toArray(new LauncherTraceEntryProto[0]);
+        return MessageNano.toByteArray(encapsulatingProto);
+    }
+
+    @Override
+    public byte[] getProtoBytes(MessageNano proto) {
+        return MessageNano.toByteArray(proto);
+    }
+
+    @Override
+    public int getProtoSize(MessageNano proto) {
+        return proto.getCachedSize();
+    }
+
+    public void start() {
+        mProtoTracer.start();
+    }
+
+    public void stop() {
+        mProtoTracer.stop();
+    }
+
+    public void add(ProtoTraceable<LauncherTraceProto> traceable) {
+        mProtoTracer.add(traceable);
+    }
+
+    public void remove(ProtoTraceable<LauncherTraceProto> traceable) {
+        mProtoTracer.remove(traceable);
+    }
+
+    public void scheduleFrameUpdate() {
+        mProtoTracer.scheduleFrameUpdate();
+    }
+
+    public void update() {
+        mProtoTracer.update();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
deleted file mode 100644
index b1999d7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.graphics.Rect;
-import android.util.ArraySet;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
-import com.android.systemui.shared.system.RecentsAnimationListener;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.Set;
-import java.util.function.Consumer;
-
-/**
- * Wrapper around {@link RecentsAnimationListener} which delegates callbacks to multiple listeners
- * on the main thread
- */
-public class RecentsAnimationListenerSet implements RecentsAnimationListener {
-
-    // The actual app surface is replaced by a screenshot upon recents animation cancelation when
-    // the thumbnailData exists. Launcher takes the responsibility to clean up this screenshot
-    // after app transition is finished. This delay is introduced to cover the app transition
-    // period of time.
-    private final int TRANSITION_DELAY = 100;
-
-    private final Set<SwipeAnimationListener> mListeners = new ArraySet<>();
-    private final boolean mShouldMinimizeSplitScreen;
-    private final Consumer<SwipeAnimationTargetSet> mOnFinishListener;
-    private RecentsAnimationControllerCompat mController;
-
-    private boolean mCancelled;
-
-    public RecentsAnimationListenerSet(boolean shouldMinimizeSplitScreen,
-            Consumer<SwipeAnimationTargetSet> onFinishListener) {
-        mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
-        mOnFinishListener = onFinishListener;
-    }
-
-    @UiThread
-    public void addListener(SwipeAnimationListener listener) {
-        Preconditions.assertUIThread();
-        mListeners.add(listener);
-    }
-
-    @UiThread
-    public void removeListener(SwipeAnimationListener listener) {
-        Preconditions.assertUIThread();
-        mListeners.remove(listener);
-    }
-
-    @Override
-    public final void onAnimationStart(RecentsAnimationControllerCompat controller,
-            RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
-            Rect minimizedHomeBounds) {
-        mController = controller;
-        SwipeAnimationTargetSet targetSet = new SwipeAnimationTargetSet(controller, targets,
-                homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
-                mOnFinishListener);
-
-        if (mCancelled) {
-            targetSet.cancelAnimation();
-        } else {
-            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-                for (SwipeAnimationListener listener : getListeners()) {
-                    listener.onRecentsAnimationStart(targetSet);
-                }
-            });
-        }
-    }
-
-    @Override
-    public final void onAnimationCanceled(ThumbnailData thumbnailData) {
-        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            for (SwipeAnimationListener listener : getListeners()) {
-                listener.onRecentsAnimationCanceled();
-            }
-        });
-        // TODO: handle the transition better instead of simply using a transition delay.
-        if (thumbnailData != null) {
-            MAIN_EXECUTOR.getHandler().postDelayed(() -> mController.cleanupScreenshot(),
-                    TRANSITION_DELAY);
-        }
-    }
-
-    private SwipeAnimationListener[] getListeners() {
-        return mListeners.toArray(new SwipeAnimationListener[mListeners.size()]);
-    }
-
-    public void cancelListener() {
-        mCancelled = true;
-        onAnimationCanceled(null);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
new file mode 100644
index 0000000..5b0d503
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.views.RecentsView;
+
+public class RecentsAtomicAnimationFactory<ACTIVITY_TYPE extends StatefulActivity, STATE_TYPE>
+        extends AtomicAnimationFactory<STATE_TYPE> {
+
+    public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
+    public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
+
+    private static final int MY_ANIM_COUNT = 2;
+    protected static final int NEXT_INDEX = AtomicAnimationFactory.NEXT_INDEX + MY_ANIM_COUNT;
+
+    protected final ACTIVITY_TYPE mActivity;
+
+    /**
+     * @param extraAnims number of animations supported by the subclass. This should not include
+     *                  the 2 animations supported by this class.
+     */
+    public RecentsAtomicAnimationFactory(ACTIVITY_TYPE activity, int extraAnims) {
+        super(MY_ANIM_COUNT + extraAnims);
+        mActivity = activity;
+    }
+
+    @Override
+    public Animator createStateElementAnimation(int index, float... values) {
+        switch (index) {
+            case INDEX_RECENTS_FADE_ANIM:
+                return ObjectAnimator.ofFloat(mActivity.getOverviewPanel(),
+                        RecentsView.CONTENT_ALPHA, values);
+            case INDEX_RECENTS_TRANSLATE_X_ANIM: {
+                RecentsView rv = mActivity.getOverviewPanel();
+                return new SpringAnimationBuilder(mActivity)
+                        .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
+                        .setDampingRatio(0.8f)
+                        .setStiffness(250)
+                        .setValues(values)
+                        .build(rv, ADJACENT_PAGE_OFFSET);
+            }
+            default:
+                return super.createStateElementAnimation(index, values);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
index c6eafe6..e5d2c53 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -16,7 +16,7 @@
 package com.android.quickstep.util;
 
 import android.animation.Animator;
-import android.content.res.Resources;
+import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.RectF;
 
@@ -28,6 +28,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.FlingSpringAnim;
+import com.android.launcher3.util.DynamicResource;
+import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
+import com.android.systemui.plugins.ResourceProvider;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -37,7 +40,7 @@
  * Applies spring forces to animate from a starting rect to a target rect,
  * while providing update callbacks to the caller.
  */
-public class RectFSpringAnim {
+public class RectFSpringAnim extends ReleaseCheck {
 
     private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
             new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
@@ -103,7 +106,7 @@
     private float mMinVisChange;
     private float mYOvershoot;
 
-    public RectFSpringAnim(RectF startRect, RectF targetRect, Resources resources) {
+    public RectFSpringAnim(RectF startRect, RectF targetRect, Context context) {
         mStartRect = startRect;
         mTargetRect = targetRect;
         mCurrentCenterX = mStartRect.centerX();
@@ -111,8 +114,10 @@
         mTrackingBottomY = startRect.bottom < targetRect.bottom;
         mCurrentY = mTrackingBottomY ? mStartRect.bottom : mStartRect.top;
 
-        mMinVisChange = resources.getDimensionPixelSize(R.dimen.swipe_up_fling_min_visible_change);
-        mYOvershoot = resources.getDimensionPixelSize(R.dimen.swipe_up_y_overshoot);
+        ResourceProvider rp = DynamicResource.provider(context);
+        mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
+        mYOvershoot = rp.getDimension(R.dimen.swipe_up_y_overshoot);
+        setCanRelease(true);
     }
 
     public void onTargetPositionChanged() {
@@ -137,7 +142,12 @@
         mAnimatorListeners.add(animatorListener);
     }
 
-    public void start(PointF velocityPxPerMs) {
+    /**
+     * Starts the fling/spring animation.
+     * @param context The activity context.
+     * @param velocityPxPerMs Velocity of swipe in px/ms.
+     */
+    public void start(Context context, PointF velocityPxPerMs) {
         // Only tell caller that we ended if both x and y animations have ended.
         OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
             mRectXAnimEnded = true;
@@ -152,7 +162,7 @@
         float endX = mTargetRect.centerX();
         float minXValue = Math.min(startX, endX);
         float maxXValue = Math.max(startX, endX);
-        mRectXAnim = new FlingSpringAnim(this, RECT_CENTER_X, startX, endX,
+        mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
                 velocityPxPerMs.x * 1000, mMinVisChange, minXValue, maxXValue, 1f, onXEndListener);
 
         float startVelocityY = velocityPxPerMs.y * 1000;
@@ -162,14 +172,18 @@
         float endY = mTrackingBottomY ? mTargetRect.bottom : mTargetRect.top;
         float minYValue = Math.min(startY, endY - mYOvershoot);
         float maxYValue = Math.max(startY, endY);
-        mRectYAnim = new FlingSpringAnim(this, RECT_Y, startY, endY, startVelocityY,
+        mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, startVelocityY,
                 mMinVisChange, minYValue, maxYValue, springVelocityFactor, onYEndListener);
 
-        float minVisibleChange = 1f / mStartRect.height();
+        float minVisibleChange = Math.abs(1f / mStartRect.height());
+        ResourceProvider rp = DynamicResource.provider(context);
+        float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
+        float stiffness = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
+
         mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
                 .setSpring(new SpringForce(1f)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                .setStiffness(SpringForce.STIFFNESS_LOW))
+                .setDampingRatio(damping)
+                .setStiffness(stiffness))
                 .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
                 .setMaxValue(1f)
                 .setMinimumVisibleChange(minVisibleChange)
@@ -178,10 +192,12 @@
                     maybeOnEnd();
                 });
 
+        setCanRelease(false);
+        mAnimsStarted = true;
+
         mRectXAnim.start();
         mRectYAnim.start();
         mRectScaleAnim.start();
-        mAnimsStarted = true;
         for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
             animatorListener.onAnimationStart(null);
         }
@@ -195,9 +211,23 @@
                 mRectScaleAnim.skipToEnd();
             }
         }
+        mRectXAnimEnded = true;
+        mRectYAnimEnded = true;
+        mRectScaleAnimEnded = true;
+        maybeOnEnd();
+    }
+
+    private boolean isEnded() {
+        return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
     }
 
     private void onUpdate() {
+        if (isEnded()) {
+            // Prevent further updates from being called. This can happen between callbacks for
+            // ending the x/y/scale animations.
+            return;
+        }
+
         if (!mOnUpdateListeners.isEmpty()) {
             float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
                     mTargetRect.width());
@@ -217,8 +247,9 @@
     }
 
     private void maybeOnEnd() {
-        if (mAnimsStarted && mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) {
+        if (mAnimsStarted && isEnded()) {
             mAnimsStarted = false;
+            setCanRelease(true);
             for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
                 animatorListener.onAnimationEnd(null);
             }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
index 83bc416..85006da 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
@@ -15,22 +15,22 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
 
 /**
  * Animates the shelf between states HIDE, PEEK, and OVERVIEW.
  */
-
 public class ShelfPeekAnim {
 
     public static final Interpolator INTERPOLATOR = OVERSHOOT_1_2;
@@ -49,7 +49,7 @@
      * Animates to the given state, canceling the previous animation if it was still running.
      */
     public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
-        if (mShelfState == shelfState) {
+        if (mShelfState == shelfState || FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
             return;
         }
         mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 958ef7d..3cafd42 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -18,33 +18,38 @@
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+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 android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
 import com.android.quickstep.views.RecentsView;
-
-import java.util.ArrayList;
-import java.util.List;
+import com.android.systemui.plugins.ResourceProvider;
 
 /**
  * Creates an animation where all the workspace items are moved into their final location,
@@ -58,16 +63,13 @@
 
     private static final float MAX_VELOCITY_PX_PER_S = 22f;
 
-    private static final float DAMPING_RATIO = 0.7f;
-    private static final float STIFFNESS = 150f;
-
     private final float mVelocity;
     private final float mSpringTransY;
 
-    private final List<Animator> mAnimators = new ArrayList<>();
+    private final AnimatorSet mAnimators = new AnimatorSet();
 
     public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim) {
-        prepareToAnimate(launcher);
+        prepareToAnimate(launcher, animateOverviewScrim);
 
         mVelocity = velocity;
 
@@ -121,25 +123,26 @@
                 addStaggeredAnimationForView(child, grid.inv.numRows + 1, totalRows);
             }
 
-            View qsb = launcher.findViewById(R.id.search_container_all_apps);
-            addStaggeredAnimationForView(qsb, grid.inv.numRows + 2, totalRows);
+            if (launcher.getAppsView().getSearchUiManager()
+                    .isQsbVisible(NORMAL.getVisibleElements(launcher))) {
+                addStaggeredAnimationForView(launcher.getAppsView().getSearchView(),
+                        grid.inv.numRows + 2, totalRows);
+            }
         }
 
         if (animateOverviewScrim) {
-            addScrimAnimationForState(launcher, BACKGROUND_APP, 0);
-            addScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+            PendingAnimation pendingAnimation = new PendingAnimation(ALPHA_DURATION_MS);
+            addScrimAnimationForState(launcher, NORMAL, pendingAnimation);
+            mAnimators.play(pendingAnimation.buildAnim());
         }
 
-        AnimatorListener resetClipListener = new AnimatorListenerAdapter() {
-            int numAnimations = mAnimators.size();
+        addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
 
+        mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
+                .setDuration(ALPHA_DURATION_MS));
+        mAnimators.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                numAnimations--;
-                if (numAnimations > 0) {
-                    return;
-                }
-
                 workspace.setClipChildren(workspaceClipChildren);
                 workspace.setClipToPadding(workspaceClipToPadding);
                 cellLayout.setClipChildren(cellLayoutClipChildren);
@@ -147,39 +150,41 @@
                 hotseat.setClipChildren(hotseatClipChildren);
                 hotseat.setClipToPadding(hotseatClipToPadding);
             }
-        };
-
-        for (Animator a : mAnimators) {
-            a.addListener(resetClipListener);
-        }
+        });
     }
 
     /**
      * Setup workspace with 0 duration to prepare for our staggered animation.
      */
-    private void prepareToAnimate(Launcher launcher) {
-        LauncherStateManager stateManager = launcher.getStateManager();
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
+        StateAnimationConfig config = new StateAnimationConfig();
+        config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
+        config.duration = 0;
         // setRecentsAttachedToAppWindow() will animate recents out.
-        builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
-        stateManager.createAtomicAnimation(BACKGROUND_APP, NORMAL, builder, ANIM_ALL, 0);
-        builder.build().start();
+        launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
 
         // Stop scrolling so that it doesn't interfere with the translation offscreen.
         launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
+
+        if (animateOverviewScrim) {
+            addScrimAnimationForState(launcher, BACKGROUND_APP, NO_ANIM_PROPERTY_SETTER);
+        }
+    }
+
+    public AnimatorSet getAnimators() {
+        return mAnimators;
+    }
+
+    public StaggeredWorkspaceAnim addAnimatorListener(Animator.AnimatorListener listener) {
+        mAnimators.addListener(listener);
+        return this;
     }
 
     /**
      * Starts the animation.
      */
     public void start() {
-        for (Animator a : mAnimators) {
-            if (a instanceof SpringObjectAnimator) {
-                ((SpringObjectAnimator) a).startSpring(1f, mVelocity, null);
-            } else {
-                a.start();
-            }
-        }
+        mAnimators.start();
     }
 
     /**
@@ -196,30 +201,58 @@
         long startDelay = (long) ((invertedRow + 1) * APP_CLOSE_ROW_START_DELAY_MS);
 
         v.setTranslationY(mSpringTransY);
-        SpringObjectAnimator springTransY = new SpringObjectAnimator<>(v, VIEW_TRANSLATE_Y,
-                1f, DAMPING_RATIO, STIFFNESS, mSpringTransY, 0);
+
+        ResourceProvider rp = DynamicResource.provider(v.getContext());
+        float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
+        float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
+        ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
+                .setStiffness(stiffness)
+                .setDampingRatio(damping)
+                .setMinimumVisibleChange(1f)
+                .setStartValue(mSpringTransY)
+                .setEndValue(0)
+                .setStartVelocity(mVelocity)
+                .build(v, VIEW_TRANSLATE_Y);
         springTransY.setStartDelay(startDelay);
-        mAnimators.add(springTransY);
+        springTransY.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setTranslationY(0f);
+            }
+        });
+        mAnimators.play(springTransY);
 
         v.setAlpha(0);
         ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
         alpha.setInterpolator(LINEAR);
         alpha.setDuration(ALPHA_DURATION_MS);
         alpha.setStartDelay(startDelay);
-        mAnimators.add(alpha);
+        alpha.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setAlpha(1f);
+            }
+        });
+        mAnimators.play(alpha);
     }
 
-    private void addScrimAnimationForState(Launcher launcher, LauncherState state, long duration) {
-        AnimatorSetBuilder scrimAnimBuilder = new AnimatorSetBuilder();
-        AnimationConfig scrimAnimConfig = new AnimationConfig();
-        scrimAnimConfig.duration = duration;
-        PropertySetter scrimPropertySetter = scrimAnimConfig.getPropertySetter(scrimAnimBuilder);
-        launcher.getWorkspace().getStateTransitionAnimation().setScrim(scrimPropertySetter, state);
-        mAnimators.add(scrimAnimBuilder.build());
-        Animator fadeOverviewScrim = ObjectAnimator.ofFloat(
-                launcher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
-                state.getOverviewScrimAlpha(launcher));
-        fadeOverviewScrim.setDuration(duration);
-        mAnimators.add(fadeOverviewScrim);
+    private void addScrimAnimationForState(Launcher launcher, LauncherState state,
+            PropertySetter setter) {
+        launcher.getWorkspace().getStateTransitionAnimation().setScrim(setter, state);
+        setter.setFloat(
+                launcher.getDragLayer().getOverviewScrim(),
+                OverviewScrim.SCRIM_PROGRESS,
+                state.getOverviewScrimAlpha(launcher),
+                ACCEL_DEACCEL);
+    }
+
+    private void addDepthAnimationForState(Launcher launcher, LauncherState state, long duration) {
+        if (!(launcher instanceof BaseQuickstepLauncher)) {
+            return;
+        }
+        PendingAnimation builder = new PendingAnimation(duration);
+        DepthController depthController = ((BaseQuickstepLauncher) launcher).getDepthController();
+        depthController.setStateWithAnimation(state, new StateAnimationConfig(), builder);
+        mAnimators.play(builder.buildAnim());
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java
new file mode 100644
index 0000000..0436a1c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.systemui.shared.system.TransactionCompat.deferTransactionUntil;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.View;
+
+import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.ViewRootImplCompat;
+
+import java.util.function.Consumer;
+
+
+/**
+ * Helper class to apply surface transactions in sync with RenderThread similar to
+ *   android.view.SyncRtSurfaceTransactionApplier
+ * with some Launcher specific utility methods
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class SurfaceTransactionApplier extends ReleaseCheck {
+
+    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
+
+    private final SurfaceControl mBarrierSurfaceControl;
+    private final ViewRootImplCompat mTargetViewRootImpl;
+    private final Handler mApplyHandler;
+
+    private int mLastSequenceNumber = 0;
+
+    /**
+     * @param targetView The view in the surface that acts as synchronization anchor.
+     */
+    public SurfaceTransactionApplier(View targetView) {
+        mTargetViewRootImpl = new ViewRootImplCompat(targetView);
+        mBarrierSurfaceControl = mTargetViewRootImpl.getRenderSurfaceControl();
+        mApplyHandler = new Handler(this::onApplyMessage);
+    }
+
+    protected boolean onApplyMessage(Message msg) {
+        if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
+            setCanRelease(msg.arg1 == mLastSequenceNumber);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Schedules applying surface parameters on the next frame.
+     *
+     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
+     *               this method to avoid synchronization issues.
+     */
+    public void scheduleApply(final SurfaceParams... params) {
+        View view = mTargetViewRootImpl.getView();
+        if (view == null) {
+            return;
+        }
+
+        mLastSequenceNumber++;
+        final int toApplySeqNo = mLastSequenceNumber;
+        setCanRelease(false);
+        mTargetViewRootImpl.registerRtFrameCallback(frame -> {
+            if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
+                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+                        .sendToTarget();
+                return;
+            }
+            Transaction t = new Transaction();
+            for (int i = params.length - 1; i >= 0; i--) {
+                SurfaceParams surfaceParams = params[i];
+                if (surfaceParams.surface.isValid()) {
+                    deferTransactionUntil(t, surfaceParams.surface, mBarrierSurfaceControl, frame);
+                    surfaceParams.applyTo(t);
+                }
+            }
+            t.apply();
+            Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+                    .sendToTarget();
+        });
+
+        // Make sure a frame gets scheduled.
+        view.invalidate();
+    }
+
+    /**
+     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
+     * attached if necessary.
+     */
+    public static void create(
+            final View targetView, final Consumer<SurfaceTransactionApplier> callback) {
+        if (targetView == null) {
+            // No target view, no applier
+            callback.accept(null);
+        } else if (new ViewRootImplCompat(targetView).isValid()) {
+            // Already attached, we're good to go
+            callback.accept(new SurfaceTransactionApplier(targetView));
+        } else {
+            // Haven't been attached before we can get the view root
+            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    targetView.removeOnAttachStateChangeListener(this);
+                    callback.accept(new SurfaceTransactionApplier(targetView));
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    // Do nothing
+                }
+            });
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
deleted file mode 100644
index 3619d3a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-
-import android.graphics.Rect;
-
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.Consumer;
-
-/**
- * Extension of {@link RemoteAnimationTargetSet} with additional information about swipe
- * up animation
- */
-public class SwipeAnimationTargetSet extends RemoteAnimationTargetSet {
-
-    private final boolean mShouldMinimizeSplitScreen;
-    private final Consumer<SwipeAnimationTargetSet> mOnFinishListener;
-
-    public final RecentsAnimationControllerCompat controller;
-    public final Rect homeContentInsets;
-    public final Rect minimizedHomeBounds;
-
-    public SwipeAnimationTargetSet(RecentsAnimationControllerCompat controller,
-            RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
-            Rect minimizedHomeBounds, boolean shouldMinimizeSplitScreen,
-            Consumer<SwipeAnimationTargetSet> onFinishListener) {
-        super(targets, MODE_CLOSING);
-        this.controller = controller;
-        this.homeContentInsets = homeContentInsets;
-        this.minimizedHomeBounds = minimizedHomeBounds;
-        this.mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
-        this.mOnFinishListener = onFinishListener;
-    }
-
-    public boolean hasTargets() {
-        return unfilteredApps.length != 0;
-    }
-
-    /**
-     * Clones the target set without any actual targets. Used only when continuing a gesture after
-     * the actual recents animation has finished.
-     */
-    public SwipeAnimationTargetSet cloneWithoutTargets() {
-        return new SwipeAnimationTargetSet(controller, new RemoteAnimationTargetCompat[0],
-                homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
-                mOnFinishListener);
-    }
-
-    public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
-        mOnFinishListener.accept(this);
-        UI_HELPER_EXECUTOR.execute(() -> {
-            controller.setInputConsumerEnabled(false);
-            controller.finish(toRecents, sendUserLeaveHint);
-
-            if (callback != null) {
-                MAIN_EXECUTOR.execute(callback);
-            }
-        });
-    }
-
-    public void enableInputConsumer() {
-        UI_HELPER_EXECUTOR.submit(() -> {
-            controller.hideCurrentInputMethod();
-            controller.setInputConsumerEnabled(true);
-        });
-    }
-
-    public void setWindowThresholdCrossed(boolean thresholdCrossed) {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            controller.setAnimationTargetsBehindSystemBars(!thresholdCrossed);
-            if (mShouldMinimizeSplitScreen && thresholdCrossed) {
-                // NOTE: As a workaround for conflicting animations (Launcher animating the task
-                // leash, and SystemUI resizing the docked stack, which resizes the task), we
-                // currently only set the minimized mode, and not the inverse.
-                // TODO: Synchronize the minimize animation with the launcher animation
-                controller.setSplitScreenMinimized(thresholdCrossed);
-            }
-        });
-    }
-
-    public ThumbnailData screenshotTask(int taskId) {
-        return controller != null ? controller.screenshotTask(taskId) : null;
-    }
-
-    public void cancelAnimation() {
-        finishController(false /* toRecents */, null, false /* sendUserLeaveHint */);
-    }
-
-    public void finishAnimation() {
-        finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
-    }
-
-    public interface SwipeAnimationListener {
-
-        void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet);
-
-        void onRecentsAnimationCanceled();
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
new file mode 100644
index 0000000..c9ed498
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.states.RotationHelper.deltaRotation;
+import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.IntProperty;
+
+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;
+import com.android.quickstep.AnimatedFloat;
+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;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
+
+/**
+ * A utility class which emulates the layout behavior of TaskView and RecentsView
+ */
+public class TaskViewSimulator implements TransformParams.BuilderProxy {
+
+    public static final IntProperty<TaskViewSimulator> SCROLL =
+            new IntProperty<TaskViewSimulator>("scroll") {
+        @Override
+        public void setValue(TaskViewSimulator simulator, int i) {
+            simulator.setScroll(i);
+        }
+
+        @Override
+        public Integer get(TaskViewSimulator simulator) {
+            return simulator.mScrollState.scroll;
+        }
+    };
+
+    private final Rect mTmpCropRect = new Rect();
+    private final RectF mTempRectF = new RectF();
+    private final float[] mTempPoint = new float[2];
+
+    private final RecentsOrientedState mOrientationState;
+    private final Context mContext;
+    private final BaseActivityInterface mSizeStrategy;
+
+    private final Rect mTaskRect = new Rect();
+    private final PointF mPivot = new PointF();
+    private DeviceProfile mDp;
+
+    private final Matrix mMatrix = new Matrix();
+    private final Point mRunningTargetWindowPosition = new Point();
+
+    // Thumbnail view properties
+    private final Rect mThumbnailPosition = new Rect();
+    private final ThumbnailData mThumbnailData = new ThumbnailData();
+    private final PreviewPositionHelper mPositionHelper = new PreviewPositionHelper();
+    private final Matrix mInversePositionMatrix = new Matrix();
+
+    // TaskView properties
+    private final FullscreenDrawParams mCurrentFullscreenParams;
+    private float mCurveScale = 1;
+
+    // RecentsView properties
+    public final AnimatedFloat recentsViewScale = new AnimatedFloat();
+    public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
+    private final ScrollState mScrollState = new ScrollState();
+    private final int mPageSpacing;
+
+    // Cached calculations
+    private boolean mLayoutValid = false;
+    private boolean mScrollValid = false;
+
+    public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
+        mContext = context;
+        mSizeStrategy = sizeStrategy;
+
+        mOrientationState = new RecentsOrientedState(context, sizeStrategy, i -> { });
+        mOrientationState.setGestureActive(true);
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
+        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+    }
+
+    /**
+     * Sets the device profile for the current state
+     */
+    public void setDp(DeviceProfile dp) {
+        mDp = dp;
+        mOrientationState.setMultiWindowMode(mDp.isMultiWindowMode);
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int)
+     */
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        mOrientationState.update(touchRotation, displayRotation);
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
+     */
+    public void setRecentsConfiguration(Configuration configuration) {
+        mOrientationState.setActivityConfiguration(configuration);
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
+     */
+    public float getFullScreenScale() {
+        if (mDp == null) {
+            return 1;
+        }
+        mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect,
+                mOrientationState.getOrientationHandler());
+        return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
+    }
+
+    /**
+     * Sets the targets which the simulator will control
+     */
+    public void setPreview(RemoteAnimationTargetCompat runningTarget) {
+        setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets);
+        mRunningTargetWindowPosition.set(runningTarget.screenSpaceBounds.left,
+                runningTarget.screenSpaceBounds.top);
+    }
+
+    /**
+     * Sets the targets which the simulator will control
+     */
+    public void setPreviewBounds(Rect bounds, Rect insets) {
+        mThumbnailData.insets.set(insets);
+        // TODO: What is this?
+        mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
+
+        mThumbnailPosition.set(bounds);
+        mLayoutValid = false;
+    }
+
+    /**
+     * Updates the scroll for RecentsView
+     */
+    public void setScroll(int scroll) {
+        if (mScrollState.scroll != scroll) {
+            mScrollState.scroll = scroll;
+            mScrollValid = false;
+        }
+    }
+
+    /**
+     * Adds animation for all the components corresponding to transition from an app to overview
+     */
+    public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+        pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
+        pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator);
+    }
+
+    /**
+     * Returns the current clipped/visible window bounds in the window coordinate space
+     */
+    public RectF getCurrentCropRect() {
+        // Crop rect is the inverse of thumbnail matrix
+        RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+        mTempRectF.set(-insets.left, -insets.top,
+                mTaskRect.width() + insets.right, mTaskRect.height() + insets.bottom);
+        mInversePositionMatrix.mapRect(mTempRectF);
+        return mTempRectF;
+    }
+
+    public RecentsOrientedState getOrientationState() {
+        return mOrientationState;
+    }
+
+    /**
+     * Returns the current transform applied to the window
+     */
+    public Matrix getCurrentMatrix() {
+        return mMatrix;
+    }
+
+    /**
+     * Applies the rotation on the matrix to so that it maps from launcher coordinate space to
+     * window coordinate space.
+     */
+    public void applyWindowToHomeRotation(Matrix matrix) {
+        mMatrix.postTranslate(mDp.windowX, mDp.windowY);
+        postDisplayRotation(deltaRotation(
+                mOrientationState.getRecentsActivityRotation(),
+                mOrientationState.getDisplayRotation()),
+                mDp.widthPx, mDp.heightPx, matrix);
+        matrix.postTranslate(-mRunningTargetWindowPosition.x, -mRunningTargetWindowPosition.y);
+    }
+
+    /**
+     * Applies the target to the previously set parameters
+     */
+    public void apply(TransformParams params) {
+        if (mDp == null || mThumbnailPosition.isEmpty()) {
+            return;
+        }
+        if (!mLayoutValid) {
+            mLayoutValid = true;
+
+            getFullScreenScale();
+            mThumbnailData.rotation = mOrientationState.getDisplayRotation();
+
+            mPositionHelper.updateThumbnailMatrix(
+                    mThumbnailPosition, mThumbnailData,
+                    mTaskRect.width(), mTaskRect.height(),
+                    mDp, mOrientationState.getRecentsActivityRotation());
+            mPositionHelper.getMatrix().invert(mInversePositionMatrix);
+
+            PagedOrientationHandler poh = mOrientationState.getOrientationHandler();
+            mScrollState.halfPageSize =
+                    poh.getPrimaryValue(mTaskRect.width(), mTaskRect.height()) / 2;
+            mScrollState.halfScreenSize = poh.getPrimaryValue(mDp.widthPx, mDp.heightPx) / 2;
+            mScrollValid = false;
+        }
+
+        if (!mScrollValid) {
+            mScrollValid = true;
+            int start = mOrientationState.getOrientationHandler()
+                    .getPrimaryValue(mTaskRect.left, mTaskRect.top);
+            mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
+            mScrollState.updateInterpolation(start, mPageSpacing);
+            mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
+        }
+
+        float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
+        mCurrentFullscreenParams.setProgress(
+                progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+
+        // Apply thumbnail matrix
+        RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+        float scale = mCurrentFullscreenParams.mScale;
+        float taskWidth = mTaskRect.width();
+        float taskHeight = mTaskRect.height();
+
+        mMatrix.set(mPositionHelper.getMatrix());
+        mMatrix.postTranslate(insets.left, insets.top);
+        mMatrix.postScale(scale, scale);
+
+        // Apply TaskView matrix: translate, scale, scroll
+        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+        mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
+        mOrientationState.getOrientationHandler().set(
+                mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
+
+        // Apply recensView matrix
+        mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
+        applyWindowToHomeRotation(mMatrix);
+
+        // Crop rect is the inverse of thumbnail matrix
+        mTempRectF.set(-insets.left, -insets.top,
+                taskWidth + insets.right, taskHeight + insets.bottom);
+        mInversePositionMatrix.mapRect(mTempRectF);
+        mTempRectF.roundOut(mTmpCropRect);
+
+        params.applySurfaceParams(params.createSurfaceParams(this));
+    }
+
+    @Override
+    public void onBuildTargetParams(
+            Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+        builder.withMatrix(mMatrix)
+                .withWindowCrop(mTmpCropRect)
+                .withCornerRadius(getCurrentCornerRadius());
+    }
+
+    /**
+     * Returns the corner radius that should be applied to the target so that it matches the
+     * TaskView
+     */
+    public float getCurrentCornerRadius() {
+        float visibleRadius = mCurrentFullscreenParams.mCurrentDrawnCornerRadius;
+        mTempPoint[0] = visibleRadius;
+        mTempPoint[1] = 0;
+        mInversePositionMatrix.mapVectors(mTempPoint);
+
+        // Ideally we should use square-root. This is an optimization as one of the dimension is 0.
+        return Math.max(Math.abs(mTempPoint[0]), Math.abs(mTempPoint[1]));
+    }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
new file mode 100644
index 0000000..0135f74
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.util.FloatProperty;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.TransactionCompat;
+
+public class TransformParams {
+
+    public static FloatProperty<TransformParams> PROGRESS =
+            new FloatProperty<TransformParams>("progress") {
+        @Override
+        public void setValue(TransformParams params, float v) {
+            params.setProgress(v);
+        }
+
+        @Override
+        public Float get(TransformParams params) {
+            return params.getProgress();
+        }
+    };
+
+    public static FloatProperty<TransformParams> TARGET_ALPHA =
+            new FloatProperty<TransformParams>("targetAlpha") {
+        @Override
+        public void setValue(TransformParams params, float v) {
+            params.setTargetAlpha(v);
+        }
+
+        @Override
+        public Float get(TransformParams params) {
+            return params.getTargetAlpha();
+        }
+    };
+
+    private float mProgress;
+    private float mTargetAlpha;
+    private float mCornerRadius;
+    private RemoteAnimationTargets mTargetSet;
+    private SurfaceTransactionApplier mSyncTransactionApplier;
+
+    private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
+    private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
+
+    public TransformParams() {
+        mProgress = 0;
+        mTargetAlpha = 1;
+        mCornerRadius = -1;
+    }
+
+    /**
+     * Sets the progress of the transformation, where 0 is the source and 1 is the target. We
+     * automatically adjust properties such as currentRect and cornerRadius based on this
+     * progress, unless they are manually overridden by setting them on this TransformParams.
+     */
+    public TransformParams setProgress(float progress) {
+        mProgress = progress;
+        return this;
+    }
+
+    /**
+     * Sets the corner radius of the transformed window, in pixels. If unspecified (-1), we
+     * simply interpolate between the window's corner radius to the task view's corner radius,
+     * based on {@link #mProgress}.
+     */
+    public TransformParams setCornerRadius(float cornerRadius) {
+        mCornerRadius = cornerRadius;
+        return this;
+    }
+
+    /**
+     * Specifies the alpha of the transformed window. Default is 1.
+     */
+    public TransformParams setTargetAlpha(float targetAlpha) {
+        mTargetAlpha = targetAlpha;
+        return this;
+    }
+
+    /**
+     * Specifies the set of RemoteAnimationTargetCompats that are included in the transformation
+     * that these TransformParams help compute. These TransformParams generally only apply to
+     * the targetSet.apps which match the targetSet.targetMode (e.g. the MODE_CLOSING app when
+     * swiping to home).
+     */
+    public TransformParams setTargetSet(RemoteAnimationTargets targetSet) {
+        mTargetSet = targetSet;
+        return this;
+    }
+
+    /**
+     * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
+     * are computed based on these TransformParams.
+     */
+    public TransformParams setSyncTransactionApplier(
+            SurfaceTransactionApplier applier) {
+        mSyncTransactionApplier = applier;
+        return this;
+    }
+
+    /**
+     * Sets an alternate function to control transform for non-target apps. The default
+     * implementation keeps the targets visible with alpha=1
+     */
+    public TransformParams setBaseBuilderProxy(BuilderProxy proxy) {
+        mBaseBuilderProxy = proxy;
+        return this;
+    }
+
+    /**
+     * Sets an alternate function to control transform for home target. The default
+     * implementation keeps the targets visible with alpha=1
+     */
+    public TransformParams setHomeBuilderProxy(BuilderProxy proxy) {
+        mHomeBuilderProxy = proxy;
+        return this;
+    }
+
+    public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
+        RemoteAnimationTargets targets = mTargetSet;
+        SurfaceParams[] surfaceParams = new SurfaceParams[targets.unfilteredApps.length];
+        for (int i = 0; i < targets.unfilteredApps.length; i++) {
+            RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
+            SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash);
+
+            if (app.mode == targets.targetMode) {
+                if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+                    mHomeBuilderProxy.onBuildTargetParams(builder, app, this);
+                } else {
+                    // Fade out Assistant overlay.
+                    if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
+                            && app.isNotInRecents) {
+                        float progress = Utilities.boundToRange(getProgress(), 0, 1);
+                        builder.withAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress));
+                    } else {
+                        builder.withAlpha(getTargetAlpha());
+                    }
+
+                    proxy.onBuildTargetParams(builder, app, this);
+                }
+            } else {
+                mBaseBuilderProxy.onBuildTargetParams(builder, app, this);
+            }
+            surfaceParams[i] = builder.build();
+        }
+        return surfaceParams;
+    }
+
+    // Pubic getters so outside packages can read the values.
+
+    public float getProgress() {
+        return mProgress;
+    }
+
+    public float getTargetAlpha() {
+        return mTargetAlpha;
+    }
+
+    public float getCornerRadius() {
+        return mCornerRadius;
+    }
+
+    public RemoteAnimationTargets getTargetSet() {
+        return mTargetSet;
+    }
+
+    public void applySurfaceParams(SurfaceParams[] params) {
+        if (mSyncTransactionApplier != null) {
+            mSyncTransactionApplier.scheduleApply(params);
+        } else {
+            TransactionCompat t = new TransactionCompat();
+            for (SurfaceParams param : params) {
+                SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
+            }
+            t.apply();
+        }
+    }
+
+    @FunctionalInterface
+    public interface BuilderProxy {
+
+        BuilderProxy NO_OP = (builder, app, params) -> { };
+        BuilderProxy ALWAYS_VISIBLE = (builder, app, params) ->builder.withAlpha(1);
+
+        void onBuildTargetParams(SurfaceParams.Builder builder,
+                RemoteAnimationTargetCompat app, TransformParams params);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
new file mode 100644
index 0000000..29b9558
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
+ */
+public class TriggerSwipeUpTouchTracker {
+
+    private final PointF mDownPos = new PointF();
+    private final float mSquaredTouchSlop;
+    private final float mMinFlingVelocity;
+    private final boolean mDisableHorizontalSwipe;
+    private final NavBarPosition mNavBarPosition;
+    private final Runnable mOnInterceptTouch;
+    private final OnSwipeUpListener mOnSwipeUp;
+
+    private boolean mInterceptedTouch;
+    private VelocityTracker mVelocityTracker;
+
+    public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
+            NavBarPosition navBarPosition, Runnable onInterceptTouch,
+            OnSwipeUpListener onSwipeUp) {
+        mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+        mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+        mNavBarPosition = navBarPosition;
+        mDisableHorizontalSwipe = disableHorizontalSwipe;
+        mOnInterceptTouch = onInterceptTouch;
+        mOnSwipeUp = onSwipeUp;
+
+        init();
+    }
+
+    /**
+     * Reset some initial values to prepare for the next gesture.
+     */
+    public void init() {
+        mInterceptedTouch = false;
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    /**
+     * @return Whether we have passed the touch slop and are still tracking the gesture.
+     */
+    public boolean interceptedTouch() {
+        return mInterceptedTouch;
+    }
+
+    /**
+     * Track motion events to determine whether an atomic swipe up has occurred.
+     */
+    public void onMotionEvent(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            return;
+        }
+
+        mVelocityTracker.addMovement(ev);
+        switch (ev.getActionMasked()) {
+            case ACTION_DOWN: {
+                mDownPos.set(ev.getX(), ev.getY());
+                break;
+            }
+            case ACTION_MOVE: {
+                if (!mInterceptedTouch) {
+                    float displacementX = ev.getX() - mDownPos.x;
+                    float displacementY = ev.getY() - mDownPos.y;
+                    if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+                        if (mDisableHorizontalSwipe
+                                && Math.abs(displacementX) > Math.abs(displacementY)) {
+                            // Horizontal gesture is not allowed in this region
+                            endTouchTracking();
+                            break;
+                        }
+
+                        mInterceptedTouch = true;
+
+                        if (mOnInterceptTouch != null) {
+                            mOnInterceptTouch.run();
+                        }
+                    }
+                }
+                break;
+            }
+
+            case ACTION_CANCEL:
+                endTouchTracking();
+                break;
+
+            case ACTION_UP: {
+                onGestureEnd(ev);
+                endTouchTracking();
+                break;
+            }
+        }
+    }
+
+    private void endTouchTracking() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void onGestureEnd(MotionEvent ev) {
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float velocityX = mVelocityTracker.getXVelocity();
+        float velocityY = mVelocityTracker.getYVelocity();
+        float velocity = mNavBarPosition.isRightEdge()
+                ? -velocityX
+                : mNavBarPosition.isLeftEdge()
+                        ? velocityX
+                        : -velocityY;
+
+        final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
+        final boolean isSwipeUp;
+        if (wasFling) {
+            isSwipeUp = velocity > 0;
+        } else {
+            float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
+            float displacementY = ev.getY() - mDownPos.y;
+            isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+        }
+
+        if (mOnSwipeUp != null) {
+            if (isSwipeUp) {
+                mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
+            } else {
+                mOnSwipeUp.onSwipeUpCancelled();
+            }
+        }
+    }
+
+    /**
+     * Callback when the gesture ends and was determined to be a swipe from the nav bar.
+     */
+    public interface OnSwipeUpListener {
+        /**
+         * Called on touch up if a swipe up was detected.
+         * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
+         * @param finalVelocity The final velocity of the swipe.
+         */
+        void onSwipeUp(boolean wasFling, PointF finalVelocity);
+
+        /** Called on touch up if a swipe up was not detected. */
+        void onSwipeUpCancelled();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
new file mode 100644
index 0000000..0979c07
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+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.OVERSHOOT_1_7;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.util.MultiValueUpdateListener;
+
+/**
+ * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
+ */
+public class AllAppsEduView extends AbstractFloatingView {
+
+    private Launcher mLauncher;
+
+    private AnimatorSet mAnimation;
+
+    private GradientDrawable mCircle;
+    private GradientDrawable mGradient;
+
+    private int mCircleSizePx;
+    private int mPaddingPx;
+    private int mWidthPx;
+    private int mMaxHeightPx;
+
+    public AllAppsEduView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
+        mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
+        mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
+        mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
+        mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        mGradient.draw(canvas);
+        mCircle.draw(canvas);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mIsOpen = true;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mIsOpen = false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // TODO
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ALL_APPS_EDU) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        return mAnimation != null && mAnimation.isRunning();
+    }
+
+    private void playAnimation() {
+        if (mAnimation != null) {
+            return;
+        }
+        mAnimation = new AnimatorSet();
+
+        final Rect circleBoundsOg = new Rect(mCircle.getBounds());
+        final Rect gradientBoundsOg = new Rect(mGradient.getBounds());
+        final Rect temp = new Rect();
+        final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx;
+
+        // 1st: Circle alpha/scale
+        int firstPart = 600;
+        // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint
+        int secondPart = 1200;
+        int introDuration = firstPart + secondPart;
+
+        StateAnimationConfig config = new StateAnimationConfig();
+        config.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
+                0, 0.08f));
+        config.duration = secondPart;
+        config.userControlled = false;
+        AnimatorPlaybackController stateAnimationController =
+                mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config);
+        float maxAllAppsProgress = 0.15f;
+
+        ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
+        intro.setInterpolator(LINEAR);
+        intro.setDuration(introDuration);
+        intro.addUpdateListener((new MultiValueUpdateListener() {
+            FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR);
+            FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7);
+            FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN);
+            FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR);
+
+            @Override
+            public void onUpdate(float progress) {
+                temp.set(circleBoundsOg);
+                temp.offset(0, (int) -mDeltaY.value);
+                Utilities.scaleRectAboutCenter(temp, mCircleScale.value);
+                mCircle.setBounds(temp);
+                mCircle.setAlpha((int) mCircleAlpha.value);
+                mGradient.setAlpha((int) mGradientAlpha.value);
+
+                temp.set(gradientBoundsOg);
+                temp.top -= mDeltaY.value;
+                mGradient.setBounds(temp);
+                invalidate();
+
+                float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0,
+                        maxAllAppsProgress, LINEAR);
+                stateAnimationController.setPlayFraction(stateProgress);
+            }
+        }));
+        intro.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mCircle.setAlpha(0);
+                mGradient.setAlpha(0);
+            }
+        });
+        mAnimation.play(intro);
+
+        ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f);
+        closeAllApps.addUpdateListener(valueAnimator -> {
+            stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue());
+        });
+        closeAllApps.setInterpolator(FAST_OUT_SLOW_IN);
+        closeAllApps.setStartDelay(introDuration);
+        closeAllApps.setDuration(250);
+        mAnimation.play(closeAllApps);
+
+        mAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimation = null;
+                stateAnimationController.dispatchOnCancel();
+                handleClose(false);
+            }
+        });
+        mAnimation.start();
+    }
+
+    private void init(Launcher launcher) {
+        mLauncher = launcher;
+
+        int accentColor = Themes.getColorAccent(launcher);
+        mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
+                Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
+                        ? new int[] {0xB3FFFFFF, 0x00FFFFFF}
+                        : new int[] {ColorUtils.setAlphaComponent(accentColor, 127),
+                                ColorUtils.setAlphaComponent(accentColor, 0)});
+        float r = mWidthPx / 2f;
+        mGradient.setCornerRadii(new float[] {r, r, r, r, 0, 0, 0, 0});
+
+        int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
+        mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
+        mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);
+
+        DeviceProfile grid = launcher.getDeviceProfile();
+        DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
+        lp.ignoreInsets = true;
+        lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
+        lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
+        setLayoutParams(lp);
+    }
+
+    /**
+     * Shows the All Apps education view and plays the animation.
+     */
+    public static void show(Launcher launcher) {
+        final DragLayer dragLayer = launcher.getDragLayer();
+        ViewGroup parent = (ViewGroup) dragLayer.getParent();
+        AllAppsEduView view = launcher.getViewCache().getView(R.layout.all_apps_edu_view,
+                launcher, parent);
+        view.init(launcher);
+        launcher.getDragLayer().addView(view);
+        launcher.getStatsLogManager().logger().log(LAUNCHER_ALL_APPS_EDU_SHOWN);
+
+        view.requestLayout();
+        view.playAnimation();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
index 9db0c09..fd74357 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -18,25 +18,25 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Property;
+import android.util.FloatProperty;
 import android.widget.Button;
 
-import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
 
 public class ClearAllButton extends Button implements PageCallbacks {
 
-    public static final Property<ClearAllButton, Float> VISIBILITY_ALPHA =
-            new Property<ClearAllButton, Float>(Float.class, "visibilityAlpha") {
+    public static final FloatProperty<ClearAllButton> VISIBILITY_ALPHA =
+            new FloatProperty<ClearAllButton>("visibilityAlpha") {
                 @Override
                 public Float get(ClearAllButton view) {
                     return view.mVisibilityAlpha;
                 }
 
                 @Override
-                public void set(ClearAllButton view, Float visibilityAlpha) {
-                    view.setVisibilityAlpha(visibilityAlpha);
+                public void setValue(ClearAllButton view, float v) {
+                    view.setVisibilityAlpha(v);
                 }
             };
 
@@ -44,21 +44,30 @@
     private float mContentAlpha = 1;
     private float mVisibilityAlpha = 1;
 
-    private final boolean mIsRtl;
+    private boolean mIsRtl;
 
     private int mScrollOffset;
 
     public ClearAllButton(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mIsRtl = Utilities.isRtl(context.getResources());
+        mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        mScrollOffset = orientationHandler.getClearAllScrollOffset(getRecentsView(), mIsRtl);
+    }
 
-        RecentsView parent = (RecentsView) getParent();
-        mScrollOffset = mIsRtl ? parent.getPaddingRight() / 2 : - parent.getPaddingLeft() / 2;
+    private RecentsView getRecentsView() {
+        return (RecentsView) getParent();
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
     }
 
     @Override
@@ -82,14 +91,16 @@
 
     @Override
     public void onPageScroll(ScrollState scrollState) {
-        float width = getWidth();
-        if (width == 0) {
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
+        if (orientationSize == 0) {
             return;
         }
 
-        float shift = Math.min(scrollState.scrollFromEdge, width);
-        setTranslationX(mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift));
-        mScrollAlpha = 1 - shift / width;
+        float shift = Math.min(scrollState.scrollFromEdge, orientationSize);
+        float translation = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
+        orientationHandler.setPrimaryAndResetSecondaryTranslate(this, translation);
+        mScrollAlpha = 1 - shift / orientationSize;
         updateAlpha();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
index eb8da6e..7cc00b7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
@@ -21,12 +21,12 @@
 import android.util.AttributeSet;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.FastBitmapDrawable;
 
 import java.util.ArrayList;
 
-import androidx.annotation.NonNull;
-
 /**
  * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
  * when the drawable changes.
@@ -130,4 +130,14 @@
             mScaleListeners.remove(listener);
         }
     }
+
+    @Override
+    public void setAlpha(float alpha) {
+        super.setAlpha(alpha);
+        if (alpha > 0) {
+            setVisibility(VISIBLE);
+        } else {
+            setVisibility(INVISIBLE);
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index c2cb720..846b944 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -19,7 +19,8 @@
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.RECENTS_CLEAR_ALL_BUTTON;
+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;
@@ -29,39 +30,38 @@
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
 import android.os.Build;
+import android.os.UserHandle;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
-import android.view.View;
+import android.view.Surface;
 import android.widget.FrameLayout;
 
-import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.Hotseat;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.StateListener;
-import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
-import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.RecentsExtraCard;
+import com.android.systemui.shared.recents.model.Task;
 
 /**
  * {@link RecentsView} used in Launcher activity
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherRecentsView extends RecentsView<Launcher> implements StateListener {
-
-    private static final Rect sTempRect = new Rect();
+public class LauncherRecentsView extends RecentsView<BaseQuickstepLauncher>
+        implements StateListener<LauncherState> {
 
     private final TransformParams mTransformParams = new TransformParams();
 
@@ -93,14 +93,25 @@
     }
 
     public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        setContentAlpha(0);
+        super(context, attrs, defStyleAttr, LauncherActivityInterface.INSTANCE);
         mActivity.getStateManager().addStateListener(this);
     }
 
     @Override
+    public void init(OverviewActionsView actionsView) {
+        super.init(actionsView);
+        setContentAlpha(0);
+    }
+
+    @Override
     public void startHome() {
-        mActivity.getStateManager().goToState(NORMAL);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            switchToScreenshot(null,
+                    () -> finishRecentsAnimation(true /* toRecents */,
+                            () -> mActivity.getStateManager().goToState(NORMAL)));
+        } else {
+            mActivity.getStateManager().goToState(NORMAL);
+        }
     }
 
     @Override
@@ -114,31 +125,12 @@
         }
     }
 
-    @Override
-    public void draw(Canvas canvas) {
-        maybeDrawEmptyMessage(canvas);
-        super.draw(canvas);
-    }
-
-    @Override
-    public void onViewAdded(View child) {
-        super.onViewAdded(child);
-        updateEmptyMessage();
-    }
-
-    @Override
-    protected void onTaskStackUpdated() {
-        // Lazily update the empty message only when the task stack is reapplied
-        updateEmptyMessage();
-    }
-
     /**
      * Animates adjacent tasks and translate hotseat off screen as well.
      */
     @Override
-    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv,
-            ClipAnimationHelper helper) {
-        AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper);
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
+        AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv);
 
         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
             // Hotseat doesn't move when opening recents with the button,
@@ -157,7 +149,7 @@
                 mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
 
         ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt(
-                mActivity.findViewById(R.id.scrim_view), ScrimView.DRAG_HANDLE_ALPHA, 0);
+                mActivity.getScrimView(), ScrimView.DRAG_HANDLE_ALPHA, 0);
         dragHandleAnim.setInterpolator(Interpolators.ACCEL_2);
         anim.play(dragHandleAnim);
 
@@ -165,38 +157,12 @@
     }
 
     @Override
-    protected void getTaskSize(DeviceProfile dp, Rect outRect) {
-        LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
-    }
-
-    /**
-     * @return The translationX to apply to this view so that the first task is just offscreen.
-     */
-    public float getOffscreenTranslationX(float recentsScale) {
-        float offscreenX = NORMAL.getOverviewScaleAndTranslation(mActivity).translationX;
-        // Offset since scale pushes tasks outwards.
-        getTaskSize(sTempRect);
-        int taskWidth = sTempRect.width();
-        offscreenX += taskWidth * (recentsScale - 1) / 2;
-        if (mRunningTaskTileHidden) {
-            // The first task is hidden, so offset by its width.
-            offscreenX -= (taskWidth + getPageSpacing()) * recentsScale;
-        }
-        if (isRtl()) {
-            offscreenX = -offscreenX;
-        }
-        return offscreenX;
-    }
-
-    @Override
     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (mRecentsAnimationWrapper.targetSet != null && tv.isRunningTask()) {
+            if (tv.isRunningTask()) {
                 mTransformParams.setProgress(1 - progress)
-                        .setSyncTransactionApplier(mSyncTransactionApplier)
-                        .setForLiveTile(true);
-                mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
-                        mTransformParams);
+                        .setSyncTransactionApplier(mSyncTransactionApplier);
+                // TODO: Revisit live tiles
             } else {
                 redrawLiveTile(true);
             }
@@ -204,19 +170,26 @@
     }
 
     @Override
-    protected void onTaskLaunched(boolean success) {
+    protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             mActivity.getStateManager().goToState(NORMAL, false /* animate */);
         } else {
             LauncherState state = mActivity.getStateManager().getState();
             mActivity.getAllAppsController().setState(state);
         }
-        super.onTaskLaunched(success);
+        super.onTaskLaunchAnimationEnd(success);
+    }
+
+    @Override
+    public void onTaskLaunched(Task task) {
+        UserHandle user =  UserHandle.of(task.key.userId);
+        AppLaunchTracker.INSTANCE.get(getContext()).onStartApp(task.getTopComponent(), user,
+                AppLaunchTracker.CONTAINER_OVERVIEW);
     }
 
     @Override
     public boolean shouldUseMultiWindowTaskSizeStrategy() {
-        return mActivity.isInMultiWindowMode();
+        return TraceHelper.whitelistIpcs("isInMultiWindowMode", mActivity::isInMultiWindowMode);
     }
 
     @Override
@@ -228,10 +201,11 @@
     }
 
     @Override
-    public void redrawLiveTile(boolean mightNeedToRefill) {
-        if (!mEnableDrawingLiveTile || mRecentsAnimationWrapper == null
-                || mClipAnimationHelper == null) {
-            return;
+    public TransformParams getLiveTileParams(
+            boolean mightNeedToRefill) {
+        if (!mEnableDrawingLiveTile || mRecentsAnimationController == null
+                || mRecentsAnimationTargets == null) {
+            return null;
         }
         TaskView taskView = getRunningTaskView();
         if (taskView != null) {
@@ -250,21 +224,19 @@
             if (mightNeedToRefill && offsetY > 0) {
                 mTempRect.top -= offsetY;
             }
-            mTempRectF.set(mTempRect);
             mTransformParams.setProgress(1f)
-                    .setCurrentRectAndTargetAlpha(mTempRectF, taskView.getAlpha())
-                    .setSyncTransactionApplier(mSyncTransactionApplier);
-            if (mRecentsAnimationWrapper.targetSet != null) {
-                mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
-                        mTransformParams);
-            }
+                    .setTargetAlpha(taskView.getAlpha())
+                    .setSyncTransactionApplier(mSyncTransactionApplier)
+                    .setTargetSet(mRecentsAnimationTargets);
         }
+        return mTransformParams;
     }
 
     @Override
     public void reset() {
         super.reset();
 
+        setLayoutRotation(Surface.ROTATION_0, Surface.ROTATION_0);
         // We are moving to home or some other UI with no recents. Switch back to the home client,
         // the home predictions should have been updated when the activity was resumed.
         PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME);
@@ -282,7 +254,7 @@
             // Clean-up logic that occurs when recents is no longer in use/visible.
             reset();
         }
-        setOverlayEnabled(finalState == OVERVIEW);
+        setOverlayEnabled(finalState == OVERVIEW || finalState == OVERVIEW_MODAL_TASK);
         setFreezeViewVisibility(false);
     }
 
@@ -292,7 +264,7 @@
         if (enabled) {
             LauncherState state = mActivity.getStateManager().getState();
             boolean hasClearAllButton = (state.getVisibleElements(mActivity)
-                    & RECENTS_CLEAR_ALL_BUTTON) != 0;
+                    & OVERVIEW_BUTTONS) != 0;
             setDisallowScrollToClearAll(!hasClearAllButton);
         }
     }
@@ -312,8 +284,8 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        PluginManagerWrapper.INSTANCE.get(getContext())
-                .addPluginListener(mRecentsExtraCardPluginListener, RecentsExtraCard.class);
+        PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
+                mRecentsExtraCardPluginListener, RecentsExtraCard.class);
     }
 
     @Override
@@ -324,19 +296,19 @@
     }
 
     @Override
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         if (canComputeScrollX() && !mIsRtl) {
             return computeScrollX();
         }
-        return super.computeMinScrollX();
+        return super.computeMinScroll();
     }
 
     @Override
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         if (canComputeScrollX() && mIsRtl) {
             return computeScrollX();
         }
-        return super.computeMaxScrollX();
+        return super.computeMaxScroll();
     }
 
     private boolean canComputeScrollX() {
@@ -364,10 +336,32 @@
     }
 
     @Override
-    public void resetTaskVisuals() {
-        super.resetTaskVisuals();
+    public boolean hasRecentsExtraCard() {
+        return mRecentsExtraViewContainer != null;
+    }
+
+    @Override
+    public void setContentAlpha(float alpha) {
+        super.setContentAlpha(alpha);
         if (mRecentsExtraViewContainer != null) {
-            mRecentsExtraViewContainer.setAlpha(mContentAlpha);
+            mRecentsExtraViewContainer.setAlpha(alpha);
+        }
+    }
+
+    @Override
+    protected DepthController getDepthController() {
+        return mActivity.getDepthController();
+    }
+
+    @Override
+    public void setModalStateEnabled(boolean isModalState) {
+        super.setModalStateEnabled(isModalState);
+        if (isModalState) {
+            mActivity.getStateManager().goToState(LauncherState.OVERVIEW_MODAL_TASK);
+        } else {
+            if (mActivity.isInState(LauncherState.OVERVIEW_MODAL_TASK)) {
+                mActivity.getStateManager().goToState(LauncherState.OVERVIEW);
+            }
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
index a838797..30c9f77 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
@@ -16,6 +16,7 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
+import android.view.ViewOverlay;
 
 import com.android.launcher3.anim.Interpolators;
 
@@ -36,9 +37,11 @@
                 }
             };
 
-    private final Paint mPaint = new Paint();
+    public static final LiveTileOverlay INSTANCE = new LiveTileOverlay();
 
-    private Rect mBoundsRect = new Rect();
+    private final Paint mPaint = new Paint();
+    private final Rect mBoundsRect = new Rect();
+
     private RectF mCurrentRect;
     private float mCornerRadius;
     private Drawable mIcon;
@@ -46,8 +49,9 @@
 
     private boolean mDrawEnabled = true;
     private float mIconAnimationProgress = 0f;
+    private boolean mIsAttached;
 
-    public LiveTileOverlay() {
+    private LiveTileOverlay() {
         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
     }
 
@@ -124,6 +128,23 @@
         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/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
new file mode 100644
index 0000000..a2da398
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SHARE;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
+import com.android.quickstep.util.LayoutUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * View for showing action buttons in Overview
+ */
+public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
+        implements OnClickListener, Insettable {
+
+    private final Rect mInsets = new Rect();
+
+    @IntDef(flag = true, value = {
+            HIDDEN_UNSUPPORTED_NAVIGATION,
+            HIDDEN_DISABLED_FEATURE,
+            HIDDEN_NON_ZERO_ROTATION,
+            HIDDEN_NO_TASKS,
+            HIDDEN_GESTURE_RUNNING,
+            HIDDEN_NO_RECENTS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ActionsHiddenFlags { }
+
+    public static final int HIDDEN_UNSUPPORTED_NAVIGATION = 1 << 0;
+    public static final int HIDDEN_DISABLED_FEATURE = 1 << 1;
+    public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 2;
+    public static final int HIDDEN_NO_TASKS = 1 << 3;
+    public static final int HIDDEN_GESTURE_RUNNING = 1 << 4;
+    public static final int HIDDEN_NO_RECENTS = 1 << 5;
+
+    @IntDef(flag = true, value = {
+            DISABLED_SCROLLING,
+            DISABLED_ROTATED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ActionsDisabledFlags { }
+
+    public static final int DISABLED_SCROLLING = 1 << 0;
+    public static final int DISABLED_ROTATED = 1 << 1;
+
+    private static final int INDEX_CONTENT_ALPHA = 0;
+    private static final int INDEX_VISIBILITY_ALPHA = 1;
+    private static final int INDEX_FULLSCREEN_ALPHA = 2;
+    private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
+
+    private final MultiValueAlpha mMultiValueAlpha;
+
+    @ActionsHiddenFlags
+    private int mHiddenFlags;
+
+    @ActionsDisabledFlags
+    protected int mDisabledFlags;
+
+    protected T mCallbacks;
+
+    public OverviewActionsView(Context context) {
+        this(context, null);
+    }
+
+    public OverviewActionsView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr, 0);
+        mMultiValueAlpha = new MultiValueAlpha(this, 4);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        View share = findViewById(R.id.action_share);
+        share.setOnClickListener(this);
+        findViewById(R.id.action_screenshot).setOnClickListener(this);
+        if (ENABLE_OVERVIEW_SHARE.get()) {
+            share.setVisibility(VISIBLE);
+            findViewById(R.id.share_space).setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Set listener for callbacks on action button taps.
+     *
+     * @param callbacks for callbacks, or {@code null} to clear the listener.
+     */
+    public void setCallbacks(T callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (mCallbacks == null) {
+            return;
+        }
+        int id = view.getId();
+        if (id == R.id.action_share) {
+            mCallbacks.onShare();
+        } else if (id == R.id.action_screenshot) {
+            mCallbacks.onScreenshot();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        updateHiddenFlags(HIDDEN_DISABLED_FEATURE, !ENABLE_OVERVIEW_ACTIONS.get());
+        updateHiddenFlags(HIDDEN_UNSUPPORTED_NAVIGATION, !removeShelfFromOverview(getContext()));
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+        updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+    }
+
+    public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
+        if (enable) {
+            mHiddenFlags |= visibilityFlags;
+        } else {
+            mHiddenFlags &= ~visibilityFlags;
+        }
+        boolean isHidden = mHiddenFlags != 0;
+        mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
+        setVisibility(isHidden ? INVISIBLE : VISIBLE);
+    }
+
+    /**
+     * Updates the proper disabled flag to indicate whether OverviewActionsView should be enabled.
+     * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to enable/disable
+     * buttons individually, currently done for select button in subclass.
+     *
+     * @param disabledFlags The flag to update.
+     * @param enable        Whether to enable the disable flag: True will cause view to be disabled.
+     */
+    public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean enable) {
+        if (enable) {
+            mDisabledFlags |= disabledFlags;
+        } else {
+            mDisabledFlags &= ~disabledFlags;
+        }
+        //
+        boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
+        LayoutUtils.setViewEnabled(this, isEnabled);
+    }
+
+    public AlphaProperty getContentAlpha() {
+        return mMultiValueAlpha.getProperty(INDEX_CONTENT_ALPHA);
+    }
+
+    public AlphaProperty getVisibilityAlpha() {
+        return mMultiValueAlpha.getProperty(INDEX_VISIBILITY_ALPHA);
+    }
+
+    public AlphaProperty getFullscreenAlpha() {
+        return mMultiValueAlpha.getProperty(INDEX_FULLSCREEN_ALPHA);
+    }
+
+    /** Updates vertical margins for different navigation mode or configuration changes. */
+    public void updateVerticalMargin(Mode mode) {
+        int bottomMargin;
+        int orientation = getResources().getConfiguration().orientation;
+        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            bottomMargin = 0;
+        } else if (mode == Mode.THREE_BUTTONS) {
+            bottomMargin = getResources()
+                    .getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_three_button);
+        } else {
+            bottomMargin = getResources()
+                    .getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_gesture);
+        }
+        bottomMargin += mInsets.bottom;
+        LayoutParams params = (LayoutParams) getLayoutParams();
+        params.setMargins(
+                params.leftMargin, params.topMargin, params.rightMargin, bottomMargin);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index ca3b92a..7b24b03 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -16,54 +16,65 @@
 
 package com.android.quickstep.views;
 
-import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
+import static android.view.Surface.ROTATION_0;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.Utilities.mapToRange;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
 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.config.FeatureFlags.QUICKSTEP_SPRINGS;
+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;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
 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.views.OverviewActionsView.HIDDEN_GESTURE_RUNNING;
+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;
 
-import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.LayoutTransition;
 import android.animation.LayoutTransition.TransitionListener;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
-import android.graphics.Matrix;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Property;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -72,47 +83,62 @@
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
-import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
 import android.widget.ListView;
 
 import androidx.annotation.Nullable;
-import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PendingAnimation.EndState;
 import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.anim.SpringProperty;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.OverScroller;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewPool;
-import com.android.quickstep.RecentsAnimationWrapper;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
+import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.ViewUtils;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TransformParams;
+import com.android.systemui.plugins.ResourceProvider;
+import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.LauncherEventUtil;
 import com.android.systemui.shared.system.PackageManagerWrapper;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 
 import java.util.ArrayList;
@@ -122,9 +148,10 @@
  * A list of recent tasks.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
-        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
-        InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener {
+public abstract class RecentsView<T extends StatefulActivity> extends PagedView implements
+        Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+        InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
+        SplitScreenBounds.OnChangeListener {
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
@@ -154,14 +181,45 @@
                 }
             };
 
-    protected RecentsAnimationWrapper mRecentsAnimationWrapper;
-    protected ClipAnimationHelper mClipAnimationHelper;
-    protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
+    public static final FloatProperty<RecentsView> TASK_MODALNESS =
+            new FloatProperty<RecentsView>("taskModalness") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    recentsView.setTaskModalness(v);
+                }
+
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mTaskModalness;
+                }
+            };
+
+    public static final FloatProperty<RecentsView> ADJACENT_PAGE_OFFSET =
+            new FloatProperty<RecentsView>("adjacentPageOffset") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    if (recentsView.mAdjacentPageOffset != v) {
+                        recentsView.mAdjacentPageOffset = v;
+                        recentsView.updatePageOffsets();
+                    }
+                }
+
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mAdjacentPageOffset;
+                }
+            };
+
+    protected RecentsOrientedState mOrientationState;
+    protected final BaseActivityInterface mSizeStrategy;
+    protected RecentsAnimationController mRecentsAnimationController;
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
+    protected SurfaceTransactionApplier mSyncTransactionApplier;
     protected int mTaskWidth;
     protected int mTaskHeight;
     protected boolean mEnableDrawingLiveTile = false;
     protected final Rect mTempRect = new Rect();
-    protected final RectF mTempRectF = new RectF();
+    private final PointF mTempPointF = new PointF();
 
     private static final int DISMISS_TASK_DURATION = 300;
     private static final int ADDITION_TASK_DURATION = 200;
@@ -175,7 +233,6 @@
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
-    protected final ClipAnimationHelper mTempClipAnimationHelper;
 
     private final ScrollState mScrollState = new ScrollState();
     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
@@ -188,7 +245,9 @@
     private boolean mDwbToastShown;
     protected boolean mDisallowScrollToClearAll;
     private boolean mOverlayEnabled;
-    private boolean mFreezeViewVisibility;
+    protected boolean mFreezeViewVisibility;
+
+    private float mAdjacentPageOffset = 0;
 
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -256,15 +315,11 @@
                 }
             });
         }
-
-        @Override
-        public void onPinnedStackAnimationStarted() {
-            // Needed for activities that auto-enter PiP, which will not trigger a remote
-            // animation to be created
-            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
-        }
     };
 
+    private final PinnedStackAnimationListener mIPinnedStackAnimationListener =
+            new PinnedStackAnimationListener();
+
     // 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;
@@ -291,6 +346,12 @@
     protected float mContentAlpha = 1;
     @ViewDebug.ExportedProperty(category = "launcher")
     protected float mFullscreenProgress = 0;
+    /**
+     * How modal is the current task to be displayed, 1 means the task is fully modal and no other
+     * tasks are show. 0 means the task is displays in context in the list with other tasks.
+     */
+    @ViewDebug.ExportedProperty(category = "launcher")
+    protected float mTaskModalness = 0;
 
     // Keeps track of task id whose visual state should not be reset
     private int mIgnoreResetTaskId = -1;
@@ -302,31 +363,43 @@
     private final Point mLastMeasureSize = new Point();
     private final int mEmptyMessagePadding;
     private boolean mShowEmptyMessage;
+    private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
     private Layout mEmptyTextLayout;
-    private LiveTileOverlay mLiveTileOverlay;
+    private boolean mLiveTileOverlayAttached;
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
+    private OverviewActionsView mActionsView;
 
     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             (inMultiWindowMode) -> {
-        if (!inMultiWindowMode && mOverviewStateEnabled) {
-            // TODO: Re-enable layout transitions for addition of the unpinned task
-            reloadIfNeeded();
-        }
-    };
+                if (mOrientationState != null) {
+                    mOrientationState.setMultiWindowMode(inMultiWindowMode);
+                    setLayoutRotation(mOrientationState.getTouchRotation(),
+                            mOrientationState.getDisplayRotation());
+                    rotateAllChildTasks();
+                }
+                if (!inMultiWindowMode && mOverviewStateEnabled) {
+                    // TODO: Re-enable layout transitions for addition of the unpinned task
+                    reloadIfNeeded();
+                }
+            };
 
-    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
+    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
+            BaseActivityInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
         setEnableFreeScroll(true);
+        mSizeStrategy = sizeStrategy;
+        mActivity = BaseActivity.fromContext(context);
+        mOrientationState = new RecentsOrientedState(
+                context, mSizeStrategy, this::animateRecentsRotationInPlace);
+        mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
-        mActivity = (T) BaseActivity.fromContext(context);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
-        mTempClipAnimationHelper = new ClipAnimationHelper(context);
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -334,7 +407,7 @@
         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                 10 /* initial size */);
 
-        mIsRtl = !Utilities.isRtl(getResources());
+        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
@@ -353,6 +426,7 @@
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
         updateEmptyMessage();
+        mOrientationHandler = mOrientationState.getOrientationHandler();
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
@@ -379,14 +453,38 @@
         return null;
     }
 
-    public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
+    @Override
+    public void onTaskIconChanged(String pkg, UserHandle user) {
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            TaskView tv = getTaskViewAt(i);
+            Task task = tv.getTask();
+            if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
+                    && task.key.userId == user.getIdentifier()) {
+                task.icon = null;
+                if (tv.getIconView().getDrawable() != null) {
+                    tv.onTaskListVisibilityChanged(true /* visible */);
+                }
+            }
+        }
+    }
+
+    /**
+     * Update the thumbnail of the task.
+     * @param refreshNow Refresh immediately if it's true.
+     */
+    public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
         TaskView taskView = getTaskView(taskId);
         if (taskView != null) {
-            taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
+            taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
         }
         return taskView;
     }
 
+    /** See {@link #updateThumbnail(int, ThumbnailData, boolean)} */
+    public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
+        return updateThumbnail(taskId, thumbnailData, true /* refreshNow */);
+    }
+
     @Override
     protected void onWindowVisibilityChanged(int visibility) {
         super.onWindowVisibilityChanged(visibility);
@@ -399,7 +497,13 @@
             return;
         }
         mModel.getIconCache().clear();
-        reset();
+        unloadVisibleTaskData();
+        loadVisibleTaskData();
+    }
+
+    public void init(OverviewActionsView actionsView) {
+        mActionsView = actionsView;
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
     }
 
     @Override
@@ -409,9 +513,14 @@
         mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
-        mSyncTransactionApplier = new SyncRtSurfaceTransactionApplierCompat(this);
+        mSyncTransactionApplier = new SurfaceTransactionApplier(this);
         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
         mIdp.addOnChangeListener(this);
+        mIPinnedStackAnimationListener.setActivity(mActivity);
+        SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
+                mIPinnedStackAnimationListener);
+        mOrientationState.initListeners();
+        SplitScreenBounds.INSTANCE.addOnChangeListener(this);
     }
 
     @Override
@@ -424,6 +533,10 @@
         mSyncTransactionApplier = null;
         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
         mIdp.removeOnChangeListener(this);
+        SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
+        SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
+        mIPinnedStackAnimationListener.setActivity(null);
+        mOrientationState.destroyListeners();
     }
 
     @Override
@@ -435,6 +548,38 @@
             TaskView taskView = (TaskView) child;
             mHasVisibleTaskData.delete(taskView.getTask().key.id);
             mTaskViewPool.recycle(taskView);
+            mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+        }
+        updateTaskStartIndex(child);
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        child.setAlpha(mContentAlpha);
+        // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
+        // child direction back to match system settings.
+        child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
+        updateTaskStartIndex(child);
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
+        updateEmptyMessage();
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        maybeDrawEmptyMessage(canvas);
+        super.draw(canvas);
+    }
+
+    private void updateTaskStartIndex(View affectingView) {
+        if (!(affectingView instanceof TaskView) && !(affectingView instanceof ClearAllButton)) {
+            int childCount = getChildCount();
+
+            mTaskViewStartIndex = 0;
+            while (mTaskViewStartIndex < childCount
+                    && !(getChildAt(mTaskViewStartIndex) instanceof TaskView)) {
+                mTaskViewStartIndex++;
+            }
         }
     }
 
@@ -456,6 +601,12 @@
     public void setOverviewStateEnabled(boolean enabled) {
         mOverviewStateEnabled = enabled;
         updateTaskStackListenerState();
+        mOrientationState.setRotationWatcherEnabled(enabled);
+        if (!enabled) {
+            // Reset the running task when leaving overview since it can still have a reference to
+            // its thumbnail
+            mTmpRunningTask = null;
+        }
     }
 
     public void onDigitalWellbeingToastShown() {
@@ -467,9 +618,29 @@
         }
     }
 
+    /**
+     * Whether the Clear All button is hidden or fully visible. Used to determine if center
+     * displayed page is a task or the Clear All button.
+     *
+     * @return True = Clear All button not fully visible, center page is a task. False = Clear All
+     * button fully visible, center page is Clear All button.
+     */
+    public boolean isClearAllHidden() {
+        return mClearAllButton.getAlpha() != 1f;
+    }
+
+    @Override
+    protected void onPageBeginTransition() {
+        super.onPageBeginTransition();
+        mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
+    }
+
     @Override
     protected void onPageEndTransition() {
         super.onPageEndTransition();
+        if (isClearAllHidden()) {
+            mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+        }
         if (getNextPage() > 0) {
             setSwipeDownShouldLaunchApp(true);
         }
@@ -501,7 +672,7 @@
             case MotionEvent.ACTION_DOWN:
                 // Touch down anywhere but the deadzone around the visible clear all button and
                 // between the task views will start home on touch up
-                if (!isHandlingTouch()) {
+                if (!isHandlingTouch() && !isModal()) {
                     if (mShowEmptyMessage) {
                         mTouchDownToStartHome = true;
                     } else {
@@ -526,13 +697,20 @@
         return isHandlingTouch() || shouldStealTouchFromSiblingsBelow(ev);
     }
 
+    @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()) {
+            super.determineScrollingStart(ev, touchSlopScale);
+        }
+    }
     protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) {
         return true;
     }
 
     protected void applyLoadPlan(ArrayList<Task> tasks) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks));
+            mPendingAnimation.addEndListener((endState) -> applyLoadPlan(tasks));
             return;
         }
 
@@ -569,7 +747,7 @@
             final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
-            taskView.bind(task);
+            taskView.bind(task, mOrientationState);
         }
 
         if (mNextPage == INVALID_PAGE) {
@@ -593,6 +771,10 @@
         updateEnabledOverlays();
     }
 
+    private boolean isModal() {
+        return mTaskModalness > 0;
+    }
+
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             removeView(getTaskViewAt(i));
@@ -610,14 +792,19 @@
         return taskViewCount;
     }
 
-    protected void onTaskStackUpdated() { }
+    protected void onTaskStackUpdated() {
+        // Lazily update the empty message only when the task stack is reapplied
+        updateEmptyMessage();
+    }
 
     public void resetTaskVisuals() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             TaskView taskView = getTaskViewAt(i);
             if (mIgnoreResetTaskId != taskView.getTask().key.id) {
-                taskView.resetVisualProperties();
+                taskView.resetViewTransforms();
                 taskView.setStableAlpha(mContentAlpha);
+                taskView.setFullscreenProgress(mFullscreenProgress);
+                taskView.setModalness(mTaskModalness);
             }
         }
         if (mRunningTaskTileHidden) {
@@ -632,6 +819,7 @@
         updateCurveProperties();
         // Update the set of visible task's data
         loadVisibleTaskData();
+        setTaskModalness(0);
     }
 
     public void setFullscreenProgress(float fullscreenProgress) {
@@ -640,6 +828,9 @@
         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));
     }
 
     private void updateTaskStackListenerState() {
@@ -656,8 +847,12 @@
     @Override
     public void setInsets(Rect insets) {
         mInsets.set(insets);
+        resetPaddingFromTaskSize();
+    }
+
+    private void resetPaddingFromTaskSize() {
         DeviceProfile dp = mActivity.getDeviceProfile();
-        getTaskSize(dp, mTempRect);
+        getTaskSize(mTempRect);
         mTaskWidth = mTempRect.width();
         mTaskHeight = mTempRect.height();
 
@@ -667,10 +862,14 @@
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
     }
 
-    protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
-
     public void getTaskSize(Rect outRect) {
-        getTaskSize(mActivity.getDeviceProfile(), outRect);
+        mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
+                mOrientationHandler);
+    }
+
+    /** Gets the task size for modal state. */
+    public void getModalTaskSize(Rect outRect) {
+        mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
     }
 
     @Override
@@ -700,21 +899,15 @@
         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
             return;
         }
-        int scrollX = getScrollX();
-        final int halfPageWidth = getNormalChildWidth() / 2;
-        final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth;
-        final int halfScreenWidth = getMeasuredWidth() / 2;
-        final int pageSpacing = mPageSpacing;
-        mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX);
+        mOrientationHandler.getCurveProperties(this, mInsets, mScrollState);
+        mScrollState.scrollFromEdge =
+                mIsRtl ? mScrollState.scroll : (mMaxScroll - mScrollState.scroll);
 
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
-            float distanceFromScreenCenter = screenCenter - pageCenter;
-            float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
-            mScrollState.linearInterpolation = Math.min(1,
-                    Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
+            mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
+                    mPageSpacing);
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
@@ -792,18 +985,27 @@
 
     public abstract void startHome();
 
+    /** `true` if there is a +1 space available in overview. */
+    public boolean hasRecentsExtraCard() {
+        return false;
+    }
+
     public void reset() {
         setCurrentTask(-1);
         mIgnoreResetTaskId = -1;
         mTaskListChangeId = -1;
 
-        mRecentsAnimationWrapper = null;
-        mClipAnimationHelper = null;
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
 
         unloadVisibleTaskData();
         setCurrentPage(0);
         mDwbToastShown = false;
         mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
+        LayoutUtils.setViewEnabled(mActionsView, true);
+        if (mOrientationState.setGestureActive(false)) {
+            updateOrientationHandler();
+        }
     }
 
     public @Nullable TaskView getRunningTaskView() {
@@ -811,7 +1013,15 @@
     }
 
     public int getRunningTaskIndex() {
-        TaskView tv = getRunningTaskView();
+        return getTaskIndexForId(mRunningTaskId);
+    }
+
+    /**
+     * Get the index of the task view whose id matches {@param taskId}.
+     * @return -1 if there is no task view for the task id, else the index of the task view.
+     */
+    public int getTaskIndexForId(int taskId) {
+        TaskView tv = getTaskView(taskId);
         return tv == null ? -1 : indexOfChild(tv);
     }
 
@@ -833,11 +1043,16 @@
      */
     public void onGestureAnimationStart(int runningTaskId) {
         // This needs to be called before the other states are set since it can create the task view
+        if (mOrientationState.setGestureActive(true)) {
+            updateOrientationHandler();
+        }
+
         showCurrentTask(runningTaskId);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
         setRunningTaskIconScaledDown(true);
+        mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, true);
     }
 
     /**
@@ -846,24 +1061,74 @@
      */
     public void onSwipeUpAnimationSuccess() {
         if (getRunningTaskView() != null) {
-            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlay != null
-                    ? mLiveTileOverlay.cancelIconAnimation()
+            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlayAttached
+                    ? LiveTileOverlay.INSTANCE.cancelIconAnimation()
                     : 0f;
             animateUpRunningTaskIconScale(startProgress);
         }
         setSwipeDownShouldLaunchApp(true);
     }
 
+    private void animateRecentsRotationInPlace(int newRotation) {
+        if (mOrientationState.canRecentsActivityRotate()) {
+            // Let system take care of the rotation
+            return;
+        }
+        AnimatorSet pa = setRecentsChangedOrientation(true);
+        pa.addListener(AnimationSuccessListener.forRunnable(() -> {
+            setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
+            mActivity.getDragLayer().recreateControllers();
+            rotateAllChildTasks();
+            setRecentsChangedOrientation(false).start();
+        }));
+        pa.start();
+    }
+
+    public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) {
+        getRunningTaskIndex();
+        int runningIndex = getCurrentPage();
+        AnimatorSet as = new AnimatorSet();
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            if (runningIndex == i) {
+                continue;
+            }
+            View taskView = getTaskViewAt(i);
+            as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
+        }
+        return as;
+    }
+
+
+    private void rotateAllChildTasks() {
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            getTaskViewAt(i).setOrientationState(mOrientationState);
+        }
+    }
+
     /**
      * Called when a gesture from an app has finished.
      */
     public void onGestureAnimationEnd() {
+        if (mOrientationState.setGestureActive(false)) {
+            updateOrientationHandler();
+        }
+
+        setOnScrollChangeListener(null);
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(true);
-        setOnScrollChangeListener(null);
-        setRunningTaskViewShowScreenshot(true);
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            setRunningTaskViewShowScreenshot(true);
+        }
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
+        animateActionsViewIn();
+    }
+
+    /**
+     * Returns true if we should add a dummy taskView for the running task id
+     */
+    protected boolean shouldAddDummyTaskView(int runningTaskId) {
+        return getTaskView(runningTaskId) == null;
     }
 
     /**
@@ -873,8 +1138,8 @@
      * is called.  Also scrolls the view to this task.
      */
     public void showCurrentTask(int runningTaskId) {
-        if (getTaskView(runningTaskId) == null) {
-            boolean wasEmpty = getTaskViewCount() == 0;
+        if (shouldAddDummyTaskView(runningTaskId)) {
+            boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
             addView(taskView, mTaskViewStartIndex);
@@ -887,7 +1152,13 @@
                     new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
                     false, true, false, false, new ActivityManager.TaskDescription(), 0,
                     new ComponentName("", ""), false);
-            taskView.bind(mTmpRunningTask);
+            taskView.bind(mTmpRunningTask, mOrientationState);
+
+            // Measure and layout immediately so that the scroll values is updated instantly
+            // as the user might be quick-switching
+            measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
+                    makeMeasureSpec(getMeasuredHeight(), EXACTLY));
+            layout(getLeft(), getTop(), getRight(), getBottom());
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -926,6 +1197,10 @@
         TaskView runningTask = getRunningTaskView();
         if (runningTask != null) {
             runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
+            if (!isHidden) {
+                AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
+                        AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
+            }
         }
     }
 
@@ -972,6 +1247,14 @@
         }
     }
 
+    private void animateActionsViewIn() {
+        mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, false);
+        ObjectAnimator anim = ObjectAnimator.ofFloat(
+                mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1);
+        anim.setDuration(TaskView.SCALE_ICON_DURATION);
+        anim.start();
+    }
+
     public void animateUpRunningTaskIconScale() {
         animateUpRunningTaskIconScale(0);
     }
@@ -1033,7 +1316,7 @@
         default void onPageScroll(ScrollState scrollState) {}
     }
 
-    public static class ScrollState {
+    public static class ScrollState extends CurveProperties {
 
         /**
          * The progress from 0 to 1, where 0 is the center
@@ -1045,6 +1328,17 @@
          * The amount by which all the content is scrolled relative to the end of the list.
          */
         public float scrollFromEdge;
+
+        /**
+         * Updates linearInterpolation for the provided child position
+         */
+        public void updateInterpolation(float childStart, int pageSpacing) {
+            float pageCenter = childStart + halfPageSize;
+            float distanceFromScreenCenter = screenCenter - pageCenter;
+            float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
+            linearInterpolation = Math.min(1,
+                    Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
+        }
     }
 
     public void setIgnoreResetTask(int taskId) {
@@ -1057,30 +1351,32 @@
         }
     }
 
-    private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
-        addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
-        if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
-            addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
-                            MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
-                            SpringForce.STIFFNESS_MEDIUM,
-                            0, -taskView.getHeight()),
-                    duration, LINEAR, anim);
-        else {
-            addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
-                    duration, LINEAR, anim);
-        }
+    private void addDismissedTaskAnimations(View 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();
+        int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
+        int verticalFactor = mOrientationHandler.getTaskDismissDirectionFactor();
+
+        ResourceProvider rp = DynamicResource.provider(mActivity);
+        SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
+                .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
+                .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
+
+        anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
+                verticalFactor * secondaryTaskDimension).setDuration(duration), LINEAR, sp);
     }
 
-    private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
-                            boolean shouldLog) {
-        if (task != null) {
-            ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-            if (shouldLog) {
-                ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key);
-                mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
-                        onEndListener.logAction, Direction.UP, index, componentKey);
-                mActivity.getStatsLogManager().logTaskDismiss(this, componentKey);
-            }
+    private void removeTask(TaskView taskView, int index, EndState endState) {
+        if (taskView.getTask() != null) {
+            ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
+            ComponentKey compKey = TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key);
+            mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+                    endState.logAction, Direction.UP, index, compKey);
+            mActivity.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo())
+                    .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
         }
     }
 
@@ -1089,20 +1385,17 @@
         if (mPendingAnimation != null) {
             mPendingAnimation.finish(false, Touch.SWIPE);
         }
-        AnimatorSet anim = new AnimatorSet();
-        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+        PendingAnimation anim = new PendingAnimation(duration);
 
         int count = getPageCount();
         if (count == 0) {
-            return pendingAnimation;
+            return anim;
         }
 
         int[] oldScroll = new int[count];
-        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
-
         int[] newScroll = new int[count];
+        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
         getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
-
         int taskCount = getTaskViewCount();
         int scrollDiffPerPage = 0;
         if (count > 1) {
@@ -1115,7 +1408,7 @@
             View child = getChildAt(i);
             if (child == taskView) {
                 if (animateTaskView) {
-                    addDismissedTaskAnimations(taskView, anim, duration);
+                    addDismissedTaskAnimations(taskView, duration, anim);
                 }
             } else {
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
@@ -1139,25 +1432,22 @@
                 }
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
-                        addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
-                                MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
-                                SpringForce.STIFFNESS_MEDIUM,
-                                0, scrollDiff), duration, ACCEL, anim);
-                    } else {
-                        addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration,
-                                ACCEL, anim);
-                    }
+                    Property translationProperty = mOrientationHandler.getPrimaryViewTranslate();
 
+                    ResourceProvider rp = DynamicResource.provider(mActivity);
+                    SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
+                            .setDampingRatio(
+                                    rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
+                            .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
+                    anim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
+                            .setDuration(duration), ACCEL, sp);
                     needsCurveUpdates = true;
                 }
             }
         }
 
         if (needsCurveUpdates) {
-            ValueAnimator va = ValueAnimator.ofFloat(0, 1);
-            va.addUpdateListener((a) -> updateCurveProperties());
-            anim.play(va);
+            anim.addOnFrameCallback(this::updateCurveProperties);
         }
 
         // Add a tiny bit of translation Z, so that it draws on top of other views
@@ -1165,22 +1455,23 @@
             taskView.setTranslationZ(0.1f);
         }
 
-        mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener(new Consumer<PendingAnimation.OnEndListener>() {
+        mPendingAnimation = anim;
+        mPendingAnimation.addEndListener(new Consumer<EndState>() {
             @Override
-            public void accept(PendingAnimation.OnEndListener onEndListener) {
+            public void accept(EndState endState) {
                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
-                        taskView.isRunningTask() && onEndListener.isSuccess) {
-                    finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener));
+                        taskView.isRunningTask() && endState.isSuccess) {
+                    finishRecentsAnimation(true /* toHome */, () -> onEnd(endState));
                 } else {
-                    onEnd(onEndListener);
+                    onEnd(endState);
                 }
             }
 
-            private void onEnd(PendingAnimation.OnEndListener onEndListener) {
-                if (onEndListener.isSuccess) {
+            @SuppressWarnings("WrongCall")
+            private void onEnd(EndState endState) {
+                if (endState.isSuccess) {
                     if (shouldRemoveTask) {
-                        removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
+                        removeTask(taskView, draggedIndex, endState);
                     }
 
                     int pageToSnapTo = mCurrentPage;
@@ -1188,37 +1479,39 @@
                             pageToSnapTo == (getTaskViewCount() - 1)) {
                         pageToSnapTo -= 1;
                     }
-                    removeView(taskView);
+                    removeViewInLayout(taskView);
 
                     if (getTaskViewCount() == 0) {
-                        removeView(mClearAllButton);
+                        removeViewInLayout(mClearAllButton);
                         startHome();
                     } else {
                         snapToPageImmediately(pageToSnapTo);
                     }
+                    // Update the layout synchronously so that the position of next view is
+                    // immediately available.
+                    onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
                 }
                 resetTaskVisuals();
                 mPendingAnimation = null;
             }
         });
-        return pendingAnimation;
+        return anim;
     }
 
     public PendingAnimation createAllTasksDismissAnimation(long duration) {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
+        if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
             throw new IllegalStateException("Another pending animation is still running");
         }
-        AnimatorSet anim = new AnimatorSet();
-        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+        PendingAnimation anim = new PendingAnimation(duration);
 
         int count = getTaskViewCount();
         for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(getTaskViewAt(i), anim, duration);
+            addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
         }
 
-        mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener((onEndListener) -> {
-            if (onEndListener.isSuccess) {
+        mPendingAnimation = anim;
+        mPendingAnimation.addEndListener((endState) -> {
+            if (endState.isSuccess) {
                 // Remove all the task views now
                 ActivityManagerWrapper.getInstance().removeAllRecentTasks();
                 removeTasksViewsAndClearAllButton();
@@ -1226,13 +1519,7 @@
             }
             mPendingAnimation = null;
         });
-        return pendingAnimation;
-    }
-
-    private static void addAnim(Animator anim, long duration,
-            TimeInterpolator interpolator, AnimatorSet set) {
-        anim.setDuration(duration).setInterpolator(interpolator);
-        set.play(anim);
+        return anim;
     }
 
     private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
@@ -1248,9 +1535,8 @@
         return true;
     }
 
-    private void runDismissAnimation(PendingAnimation pendingAnim) {
-        AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
-                pendingAnim.anim, DISMISS_TASK_DURATION);
+    protected void runDismissAnimation(PendingAnimation pendingAnim) {
+        AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
         controller.dispatchOnStart();
         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
@@ -1269,7 +1555,7 @@
     }
 
     private void dismissCurrentTask() {
-        TaskView taskView = getTaskView(getNextPage());
+        TaskView taskView = getNextPageTaskView();
         if (taskView != null) {
             dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
         }
@@ -1336,10 +1622,10 @@
             }
         }
         mClearAllButton.setContentAlpha(mContentAlpha);
-
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
         mEmptyIcon.setAlpha(alphaInt);
+        mActionsView.getContentAlpha().setValue(mContentAlpha);
 
         if (alpha > 0) {
             setVisibility(VISIBLE);
@@ -1355,7 +1641,6 @@
     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
         if (mFreezeViewVisibility != freezeViewVisibility) {
             mFreezeViewVisibility = freezeViewVisibility;
-
             if (!mFreezeViewVisibility) {
                 setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
             }
@@ -1363,9 +1648,54 @@
     }
 
     @Override
-    public void onViewAdded(View child) {
-        super.onViewAdded(child);
-        child.setAlpha(mContentAlpha);
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (mActionsView != null) {
+            mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mOrientationState.setActivityConfiguration(newConfig)) {
+            updateOrientationHandler();
+        }
+    }
+
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        if (mOrientationState.update(touchRotation, displayRotation)) {
+            updateOrientationHandler();
+        }
+    }
+
+    private void updateOrientationHandler() {
+        mOrientationHandler = mOrientationState.getOrientationHandler();
+        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+        setLayoutDirection(mIsRtl
+                ? View.LAYOUT_DIRECTION_RTL
+                : View.LAYOUT_DIRECTION_LTR);
+        mClearAllButton.setLayoutDirection(mIsRtl
+                ? View.LAYOUT_DIRECTION_LTR
+                : View.LAYOUT_DIRECTION_RTL);
+        mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
+        mActivity.getDragLayer().recreateControllers();
+        boolean isInLandscape = mOrientationState.getTouchRotation() != 0
+                || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
+        mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
+                !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+        resetPaddingFromTaskSize();
+        requestLayout();
+        // Reapply the current page to update page scrolls.
+        setCurrentPage(mCurrentPage);
+    }
+
+    public RecentsOrientedState getPagedViewOrientedState() {
+        return mOrientationState;
+    }
+
+    public PagedOrientationHandler getPagedOrientationHandler() {
+        return mOrientationHandler;
     }
 
     @Nullable
@@ -1374,11 +1704,6 @@
     }
 
     @Nullable
-    public TaskView getPreviousTaskView() {
-        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
-    }
-
-    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAtByAbsoluteIndex(getCurrentPage());
     }
@@ -1410,6 +1735,10 @@
         return null;
     }
 
+    public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
+        mOnEmptyMessageUpdatedListener = listener;
+    }
+
     public void updateEmptyMessage() {
         boolean isEmpty = getTaskViewCount() == 0;
         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
@@ -1421,18 +1750,68 @@
         mShowEmptyMessage = isEmpty;
         updateEmptyStateUi(hasSizeChanged);
         invalidate();
+
+        if (mOnEmptyMessageUpdatedListener != null) {
+            mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
+        }
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
+
         updateEmptyStateUi(changed);
 
-        // Set the pivot points to match the task preview center
-        setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
-                + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2);
-        setPivotX(((mInsets.left + getPaddingLeft())
-                + (getWidth() - mInsets.right - getPaddingRight())) / 2);
+        // Update the pivots such that when the task is scaled, it fills the full page
+        getTaskSize(mTempRect);
+        getPagedViewOrientedState().getFullScreenScaleAndPivot(
+                mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+        setPivotX(mTempPointF.x);
+        setPivotY(mTempPointF.y);
+        setTaskModalness(mTaskModalness);
+        updatePageOffsets();
+        setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
+                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+    }
+
+    private void updatePageOffsets() {
+        float offset = mAdjacentPageOffset * getWidth();
+        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness) * getWidth();
+        if (mIsRtl) {
+            offset = -offset;
+            modalOffset = -modalOffset;
+        }
+        int count = getChildCount();
+
+        TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
+                ? null : getTaskView(mRunningTaskId);
+        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
+        int currentPage = getCurrentPage();
+
+        for (int i = 0; i < count; i++) {
+            float translation = i == midPoint ? 0 : (i < midPoint ? -offset : offset);
+            float modalTranslation =
+                    i == currentPage ? 0 : (i < currentPage ? -modalOffset : modalOffset);
+            getChildAt(i).setTranslationX(translation + modalTranslation);
+        }
+        updateCurveProperties();
+    }
+
+    /**
+     * TODO: Do not assume motion across X axis for adjacent page
+     */
+    public float getPageOffsetScale() {
+        return Math.max(getWidth(), 1);
+    }
+
+    /**
+     * Resets the visuals when exit modal state.
+     */
+    public void resetModalVisuals() {
+        TaskView taskView = getCurrentPageTaskView();
+        if (taskView != null) {
+            taskView.getThumbnail().getTaskOverlay().resetModalVisuals();
+        }
     }
 
     private void updateDeadZoneRects() {
@@ -1506,22 +1885,17 @@
      * If launching one of the adjacent tasks, parallax the center task and other adjacent task
      * to the right.
      */
-    public AnimatorSet createAdjacentPageAnimForTaskLaunch(
-            TaskView tv, ClipAnimationHelper clipAnimationHelper) {
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
         AnimatorSet anim = new AnimatorSet();
 
         int taskIndex = indexOfChild(tv);
         int centerTaskIndex = getCurrentPage();
         boolean launchingCenterTask = taskIndex == centerTaskIndex;
 
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = clipAnimationHelper
-                .getScaleAndTranslation();
-        float toScale = toScaleAndTranslation.scale;
-        float toTranslationY = toScaleAndTranslation.translationY;
+        float toScale = getMaxScaleForFullScreen();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
             anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
-            anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -1540,20 +1914,29 @@
         return anim;
     }
 
-    public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
+    /**
+     * Returns the scale up required on the view, so that it coves the screen completely
+     */
+    public float getMaxScaleForFullScreen() {
+        getTaskSize(mTempRect);
+        return getPagedViewOrientedState().getFullScreenScaleAndPivot(
+                mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+    }
+
+    public PendingAnimation createTaskLaunchAnimation(
+            TaskView tv, long duration, Interpolator interpolator) {
+        if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
             throw new IllegalStateException("Another pending animation is still running");
         }
 
         int count = getTaskViewCount();
         if (count == 0) {
-            return new PendingAnimation(new AnimatorSet());
+            return new PendingAnimation(duration);
         }
 
         int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
         final boolean[] passedOverviewThreshold = new boolean[] {false};
         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
-        progressAnim.setInterpolator(LINEAR);
         progressAnim.addUpdateListener(animator -> {
             // Once we pass a certain threshold, update the sysui flags to match the target
             // tasks' flags
@@ -1574,20 +1957,23 @@
             }
         });
 
-        ClipAnimationHelper clipAnimationHelper = new ClipAnimationHelper(mActivity);
-        clipAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this);
-        clipAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */);
-        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, clipAnimationHelper);
+        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
+
+        DepthController depthController = getDepthController();
+        if (depthController != null) {
+            ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
+                    BACKGROUND_APP.getDepth(mActivity));
+            anim.play(depthAnimator);
+        }
         anim.play(progressAnim);
-        anim.setDuration(duration);
+        anim.setInterpolator(interpolator);
 
-        Consumer<Boolean> onTaskLaunchFinish = this::onTaskLaunched;
-
-        mPendingAnimation = new PendingAnimation(anim);
-        mPendingAnimation.addEndListener((onEndListener) -> {
-            if (onEndListener.isSuccess) {
+        mPendingAnimation = new PendingAnimation(duration);
+        mPendingAnimation.add(anim);
+        mPendingAnimation.addEndListener((endState) -> {
+            if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
-                    onTaskLaunchFinish.accept(result);
+                    onTaskLaunchAnimationEnd(result);
                     if (!result) {
                         tv.notifyTaskLaunchFailed(TAG);
                     }
@@ -1596,11 +1982,13 @@
                 Task task = tv.getTask();
                 if (task != null) {
                     mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
-                            onEndListener.logAction, Direction.DOWN, indexOfChild(tv),
+                            endState.logAction, Direction.DOWN, indexOfChild(tv),
                             TaskUtils.getLaunchComponentKeyForTask(task.key));
+                    mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
+                            .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
                 }
             } else {
-                onTaskLaunchFinish.accept(false);
+                onTaskLaunchAnimationEnd(false);
             }
             mPendingAnimation = null;
         });
@@ -1612,12 +2000,17 @@
 
     public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
 
-    protected void onTaskLaunched(boolean success) {
+    protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             resetTaskVisuals();
         }
     }
 
+    /**
+     * Called when task activity is launched
+     */
+    public void onTaskLaunched(Task task){ }
+
     @Override
     protected void notifyPageSwitchListener(int prevPage) {
         super.notifyPageSwitchListener(prevPage);
@@ -1657,8 +2050,8 @@
 
         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
             final int[] visibleTasks = getVisibleChildrenRange();
-            event.setFromIndex(taskViewCount - visibleTasks[1] - 1);
-            event.setToIndex(taskViewCount - visibleTasks[0] - 1);
+            event.setFromIndex(taskViewCount - visibleTasks[1]);
+            event.setToIndex(taskViewCount - visibleTasks[0]);
             event.setItemCount(taskViewCount);
         }
     }
@@ -1680,33 +2073,42 @@
 
     public void redrawLiveTile(boolean mightNeedToRefill) { }
 
-    public void setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper) {
-        mRecentsAnimationWrapper = recentsAnimationWrapper;
+    // TODO: To be removed in a follow up CL
+    public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
+            RecentsAnimationTargets recentsAnimationTargets) {
+        mRecentsAnimationController = recentsAnimationController;
+        mRecentsAnimationTargets = recentsAnimationTargets;
     }
 
-    public void setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper) {
-        mClipAnimationHelper = clipAnimationHelper;
-    }
-
-    public void setLiveTileOverlay(LiveTileOverlay liveTileOverlay) {
-        mLiveTileOverlay = liveTileOverlay;
+    public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
+        mLiveTileOverlayAttached = liveTileOverlayAttached;
     }
 
     public void updateLiveTileIcon(Drawable icon) {
-        if (mLiveTileOverlay != null) {
-            mLiveTileOverlay.setIcon(icon);
+        if (mLiveTileOverlayAttached) {
+            LiveTileOverlay.INSTANCE.setIcon(icon);
         }
     }
 
     public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
-        if (mRecentsAnimationWrapper == null) {
+        if (mRecentsAnimationController == null) {
             if (onFinishComplete != null) {
                 onFinishComplete.run();
             }
             return;
         }
 
-        mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
+        mRecentsAnimationController.finish(toRecents, () -> {
+            if (onFinishComplete != null) {
+                onFinishComplete.run();
+                // After we finish the recents animation, the current task id should be correctly
+                // reset so that when the task is launched from Overview later, it goes through the
+                // flow of starting a new task instead of finishing recents animation to app. A
+                // typical example of this is (1) user swipes up from app to Overview (2) user
+                // taps on QSB (3) user goes back to Overview and launch the most recent task.
+                setCurrentTask(-1);
+            }
+        });
     }
 
     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
@@ -1717,7 +2119,7 @@
     }
 
     @Override
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
                 // We aren't showing the clear all button,
@@ -1732,11 +2134,11 @@
             }
             return getScrollForPage(mTaskViewStartIndex);
         }
-        return super.computeMinScrollX();
+        return super.computeMinScroll();
     }
 
     @Override
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
                 // We aren't showing the clear all button,
@@ -1751,57 +2153,76 @@
             }
             return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
         }
-        return super.computeMaxScrollX();
+        return super.computeMaxScroll();
     }
 
     public ClearAllButton getClearAllButton() {
         return mClearAllButton;
     }
 
+    @Override
+    protected boolean onOverscroll(int amount) {
+        // overscroll should only be accepted on -1 direction (for clear all button)
+        if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
+        return super.onOverscroll(amount);
+    }
+
     /**
-     * @return How many pixels the running task is offset on the x-axis due to the current scrollX.
+     * @return How many pixels the running task is offset on the currently laid out dominant axis.
      */
-    public float getScrollOffset() {
-        if (getRunningTaskIndex() == -1) {
+    public int getScrollOffset() {
+        return getScrollOffset(getRunningTaskIndex());
+    }
+
+    /**
+     * @return How many pixels the page is offset on the currently laid out dominant axis.
+     */
+    public int getScrollOffset(int pageIndex) {
+        if (pageIndex == -1) {
             return 0;
         }
-        int startScroll = getScrollForPage(getRunningTaskIndex());
-        int offsetX = startScroll - getScrollX();
-        offsetX *= getScaleX();
-        return offsetX;
+        return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this);
     }
 
-    public Consumer<MotionEvent> getEventDispatcher(RotationMode rotationMode) {
-        if (rotationMode.isTransposed) {
-            Matrix transform = new Matrix();
-            transform.setRotate(-rotationMode.surfaceRotation);
-
-            if (getWidth() > 0 && getHeight() > 0) {
-                float scale = ((float) getWidth()) / getHeight();
-                transform.postScale(scale, 1 / scale);
-            }
-
-            Matrix inverse = new Matrix();
-            transform.invert(inverse);
-            return e -> {
-                e.transform(transform);
-                super.onTouchEvent(e);
-                e.transform(inverse);
-            };
+    public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
+        float degreesRotated;
+        if (navbarRotation == 0) {
+            degreesRotated = mOrientationHandler.getDegreesRotated();
         } else {
+            degreesRotated = -navbarRotation;
+        }
+        if (degreesRotated == 0) {
             return super::onTouchEvent;
         }
+
+        // At this point the event coordinates have already been transformed, so we need to
+        // undo that transformation since PagedView also accommodates for the transformation via
+        // PagedOrientationHandler
+        return e -> {
+            if (navbarRotation != 0
+                    && mOrientationState.isMultipleOrientationSupportedByDevice()
+                    && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
+                mOrientationState.flipVertical(e);
+                super.onTouchEvent(e);
+                mOrientationState.flipVertical(e);
+                return;
+            }
+            mOrientationState.transformEvent(-degreesRotated, e, true);
+            super.onTouchEvent(e);
+            mOrientationState.transformEvent(-degreesRotated, e, false);
+        };
     }
 
-    public ClipAnimationHelper getTempClipAnimationHelper() {
-        return mTempClipAnimationHelper;
+    public TransformParams getLiveTileParams(
+            boolean mightNeedToRefill) {
+        return null;
     }
 
     private void updateEnabledOverlays() {
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
-        for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
+        for (int i = mTaskViewStartIndex; i < mTaskViewStartIndex + taskCount; i++) {
+            getTaskViewAtByAbsoluteIndex(i).setOverlayEnabled(i == overlayEnabledPage);
         }
     }
 
@@ -1812,34 +2233,84 @@
         }
     }
 
-    public int getLeftGestureMargin() {
-        final WindowInsets insets = getRootWindowInsets();
-        return Math.max(insets.getSystemGestureInsets().left, insets.getSystemWindowInsetLeft());
-    }
-
-    public int getRightGestureMargin() {
-        final WindowInsets insets = getRootWindowInsets();
-        return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight());
-    }
-
-    @Override
-    public void addView(View child, int index) {
-        super.addView(child, index);
-        if (isExtraCardView(child, index)) {
-            mTaskViewStartIndex++;
+    /** If it's in the live tile mode, switch the running task into screenshot mode. */
+    public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) {
+        TaskView taskView = getRunningTaskView();
+        if (taskView != null) {
+            taskView.setShowScreenshot(true);
+            if (thumbnailData != null) {
+                taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
+            } else {
+                taskView.getThumbnail().refresh();
+            }
+            ViewUtils.postDraw(taskView, onFinishRunnable);
+        } else {
+            onFinishRunnable.run();
         }
     }
 
-    @Override
-    public void removeView(View view) {
-        if (isExtraCardView(view, indexOfChild(view))) {
-            mTaskViewStartIndex--;
+    /**
+     * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
+     * way. Modalness 0 means the task is shown in context with all the other tasks.
+     */
+    private void setTaskModalness(float modalness) {
+        mTaskModalness = modalness;
+        updatePageOffsets();
+        if (getCurrentPageTaskView() != null) {
+            getCurrentPageTaskView().setModalness(modalness);
         }
-        super.removeView(view);
+        // Only show actions view when it's modal for in-place landscape mode.
+        boolean inPlaceLandscape = !mOrientationState.canRecentsActivityRotate()
+                && mOrientationState.getTouchRotation() != ROTATION_0;
+        mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
     }
 
-    private boolean isExtraCardView(View view, int index) {
-        return !(view instanceof TaskView) && !(view instanceof ClearAllButton)
-                && index <= mTaskViewStartIndex;
+    @Nullable
+    protected DepthController getDepthController() {
+        return null;
+    }
+
+    @Override
+    public void onSecondaryWindowBoundsChanged() {
+        // Invalidate the task view size
+        setInsets(mInsets);
+        requestLayout();
+    }
+
+    /**
+     * Enables or disables modal state for RecentsView
+     * @param isModalState
+     */
+    public void setModalStateEnabled(boolean isModalState) { }
+
+    public BaseActivityInterface getSizeStrategy() {
+        return mSizeStrategy;
+    }
+
+    /**
+     * Used to register callbacks for when our empty message state changes.
+     *
+     * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
+     * @see #updateEmptyMessage()
+     */
+    public interface OnEmptyMessageUpdatedListener {
+        /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
+        void onEmptyMessageUpdated(boolean isEmpty);
+    }
+
+    private static class PinnedStackAnimationListener<T extends BaseActivity> extends
+            IPinnedStackAnimationListener.Stub {
+        private T mActivity;
+
+        public void setActivity(T activity) {
+            mActivity = activity;
+        }
+
+        @Override
+        public void onPinnedStackAnimationStarted() {
+            // Needed for activities that auto-enter PiP, which will not trigger a remote
+            // animation to be created
+            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index 07d0796..ef66b7a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -26,7 +25,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
@@ -41,16 +39,14 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.testing.TestProtocol;
+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.TaskSystemShortcut;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.views.IconView.OnScaleUpdateListener;
 
-import java.util.List;
-
 /**
  * Contains options for a recent task when long-pressing its icon.
  */
@@ -155,9 +151,26 @@
         return (type & TYPE_TASK_MENU) != 0;
     }
 
-    public void setPosition(float x, float y) {
-        setX(x);
-        setY(y + mThumbnailTopMargin);
+    public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) {
+        float adjustedY = y + mThumbnailTopMargin;
+        // Changing pivot to make computations easier
+        // 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);
+        setRotation(pagedOrientationHandler.getDegreesRotated());
+        setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
+        setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
+    }
+
+    public void onRotationChanged() {
+        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+            mOpenCloseAnimator.end();
+        }
+        if (mIsOpen) {
+            mOptionLayout.removeAllViews();
+            populateAndLayoutMenu();
+        }
     }
 
     public static TaskMenuView showForTask(TaskView taskView) {
@@ -173,12 +186,16 @@
         }
         mActivity.getDragLayer().addView(this);
         mTaskView = taskView;
-        addMenuOptions(mTaskView);
-        orientAroundTaskView(mTaskView);
+        populateAndLayoutMenu();
         post(this::animateOpen);
         return true;
     }
 
+    private void populateAndLayoutMenu() {
+        addMenuOptions(mTaskView);
+        orientAroundTaskView(mTaskView);
+    }
+
     private void addMenuOptions(TaskView taskView) {
         Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
         mTaskIcon.setDrawable(icon);
@@ -197,36 +214,36 @@
         params.topMargin = (int) -mThumbnailTopMargin;
         mTaskIcon.setLayoutParams(params);
 
-        final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext());
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(taskView);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            addMenuOption(menuOption, menuOption.getOnClickListener(activity, taskView));
-        }
+        TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
     }
 
-    private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) {
+    private void addMenuOption(SystemShortcut menuOption) {
         ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate(
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
-        menuOptionView.setOnClickListener(onClickListener);
+        LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
+        mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
+        menuOptionView.setOnClickListener(menuOption);
         mOptionLayout.addView(menuOptionView);
     }
 
     private void orientAroundTaskView(TaskView taskView) {
+        PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler();
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
         mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
         Rect insets = mActivity.getDragLayer().getInsets();
         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
-        params.width = taskView.getMeasuredWidth();
-        params.gravity = Gravity.START;
+        params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail());
+        // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
+        params.gravity = Gravity.LEFT;
         setLayoutParams(params);
         setScaleX(taskView.getScaleX());
         setScaleY(taskView.getScaleY());
-        setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top);
+        mOptionLayout.setOrientation(orientationHandler
+                .getTaskMenuLayoutOrientation(mOptionLayout));
+        setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
+            taskView.getPagedOrientationHandler());
     }
 
     private void animateOpen() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 044292a..b2f937f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -20,7 +20,6 @@
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
@@ -28,6 +27,7 @@
 import android.graphics.ColorFilter;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
@@ -35,36 +35,42 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Shader;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Property;
+import android.view.Surface;
 import android.view.View;
-import android.view.ViewGroup;
+
+import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.BaseActivity;
 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;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
-import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.plugins.OverviewScreenshotActions;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ConfigurationCompat;
 
 /**
  * A task in the Recents view.
  */
 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
 
-    private final static ColorMatrix COLOR_MATRIX = new ColorMatrix();
-    private final static ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
-    private final static RectF EMPTY_RECT_F = new RectF();
+    private static final ColorMatrix COLOR_MATRIX = new ColorMatrix();
+    private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
+
+    private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
+            new MainThreadInitializedObject<>(FullscreenDrawParams::new);
 
     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
             new FloatProperty<TaskThumbnailView>("dimAlpha") {
@@ -87,11 +93,9 @@
     private final Paint mClearPaint = new Paint();
     private final Paint mDimmingPaintAfterClearing = new Paint();
 
-    private final Matrix mMatrix = new Matrix();
-
-    private float mClipBottom = -1;
     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
-    private RectF mClippedInsets = new RectF();
+    private final Rect mPreviewRect = new Rect();
+    private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
     private TaskView.FullscreenDrawParams mFullscreenParams;
 
     private Task mTask;
@@ -103,7 +107,6 @@
     private float mSaturation = 1f;
 
     private boolean mOverlayEnabled;
-    private boolean mRotated;
     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
     public TaskThumbnailView(Context context) {
@@ -123,9 +126,14 @@
         mDimmingPaintAfterClearing.setColor(Color.BLACK);
         mActivity = BaseActivity.fromContext(context);
         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
-        mFullscreenParams = new TaskView.FullscreenDrawParams(TaskCornerRadius.get(context));
+        // Initialize with dummy value. It is overridden later by TaskView
+        mFullscreenParams = TEMP_PARAMS.get(context);
     }
 
+    /**
+     * Updates the thumbnail to draw the provided task
+     * @param task
+     */
     public void bind(Task task) {
         mOverlay.reset();
         mTask = task;
@@ -135,16 +143,35 @@
     }
 
     /**
-     * Updates this thumbnail.
+     * Updates the thumbnail.
+     * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
+     *                   In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
+     *                   version with {@code refreshNow} is true. The only exception is
+     *                   in the live tile case that we grab a screenshot when user enters Overview
+     *                   upon swipe up so that a usable screenshot is accessible immediately when
+     *                   recents animation needs to be finished / cancelled.
      */
-    public void setThumbnail(Task task, ThumbnailData thumbnailData) {
+    public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) {
         mTask = task;
-        if (thumbnailData != null && thumbnailData.thumbnail != null) {
-            Bitmap bm = thumbnailData.thumbnail;
+        mThumbnailData =
+                (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
+        if (refreshNow) {
+            refresh();
+        }
+    }
+
+    /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
+    public void setThumbnail(Task task, ThumbnailData thumbnailData) {
+        setThumbnail(task, thumbnailData, true /* refreshNow */);
+    }
+
+    /** Updates the shader, paint, matrix to redraw. */
+    public void refresh() {
+        if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
+            Bitmap bm = mThumbnailData.thumbnail;
             bm.prepareToDraw();
             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
             mPaint.setShader(mBitmapShader);
-            mThumbnailData = thumbnailData;
             updateThumbnailMatrix();
         } else {
             mBitmapShader = null;
@@ -152,10 +179,8 @@
             mPaint.setShader(null);
             mOverlay.reset();
         }
-
         if (mOverviewScreenshotActionsPlugin != null) {
-            mOverviewScreenshotActionsPlugin
-                .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
+            mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
         }
         updateThumbnailPaintFilter();
     }
@@ -175,9 +200,8 @@
         updateThumbnailPaintFilter();
     }
 
-    public void setSaturation(float saturation) {
-        mSaturation = saturation;
-        updateThumbnailPaintFilter();
+    public TaskOverlay getTaskOverlay() {
+        return mOverlay;
     }
 
     public float getDimAlpha() {
@@ -191,6 +215,38 @@
         return fallback;
     }
 
+    /**
+     * Get the scaled insets that are being used to draw the task view. This is a subsection of
+     * the full snapshot.
+     * @return the insets in snapshot bitmap coordinates.
+     */
+    @RequiresApi(api = Build.VERSION_CODES.Q)
+    public Insets getScaledInsets() {
+        if (mThumbnailData == null) {
+            return Insets.NONE;
+        }
+
+        RectF bitmapRect = new RectF(
+                0, 0,
+                mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
+        RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
+
+        // The position helper matrix tells us how to transform the bitmap to fit the view, the
+        // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
+        // difference between the bitmap bounds and the projected view bounds.
+        Matrix boundsToBitmapSpace = new Matrix();
+        mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
+        RectF boundsInBitmapSpace = new RectF();
+        boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
+
+        return Insets.of(
+            Math.round(boundsInBitmapSpace.left),
+            Math.round(boundsInBitmapSpace.top),
+            Math.round(bitmapRect.right - boundsInBitmapSpace.right),
+            Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom));
+    }
+
+
     public int getSysUiStatusNavFlags() {
         if (mThumbnailData != null) {
             int flags = 0;
@@ -209,8 +265,8 @@
     protected void onDraw(Canvas canvas) {
         RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
         canvas.save();
-        canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
         canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
+        canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
         drawOnCanvas(canvas,
                 -currentDrawnInsets.left,
@@ -248,9 +304,8 @@
         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
     }
 
-    public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
-        // Don't show insets in multi window mode.
-        return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
+    public PreviewPositionHelper getPreviewPositionHelper() {
+        return mPreviewPositionHelper;
     }
 
     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
@@ -272,16 +327,17 @@
         // Draw the background in all cases, except when the thumbnail data is opaque
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
-        if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) {
+        if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
+                || mThumbnailData.isTranslucent) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
             if (drawBackgroundOnly) {
                 return;
             }
         }
 
-        if (mClipBottom > 0) {
+        if (mPreviewPositionHelper.mClipBottom > 0) {
             canvas.save();
-            canvas.clipRect(x, y, width, mClipBottom);
+            canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
             canvas.restore();
         } else {
@@ -301,9 +357,9 @@
     }
 
     private void updateOverlay() {
-        // The overlay doesn't really work when the screenshot is rotated, so don't add it.
-        if (mOverlayEnabled && !mRotated && mBitmapShader != null && mThumbnailData != null) {
-            mOverlay.initOverlay(mTask, mThumbnailData, mMatrix);
+        if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
+            mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
+                    mPreviewPositionHelper.mIsOrientationChanged);
         } else {
             mOverlay.reset();
         }
@@ -324,86 +380,21 @@
     }
 
     private void updateThumbnailMatrix() {
-        boolean isRotated = false;
-        mClipBottom = -1;
+        mPreviewPositionHelper.mClipBottom = -1;
+        mPreviewPositionHelper.mIsOrientationChanged = false;
         if (mBitmapShader != null && mThumbnailData != null) {
-            float scale = mThumbnailData.scale;
-            Rect thumbnailInsets = mThumbnailData.insets;
-            final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
-                    (thumbnailInsets.left + thumbnailInsets.right) * scale;
-            final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
-                    (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+            mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
+                    mThumbnailData.thumbnail.getHeight());
+            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
+                    mActivity.getResources().getConfiguration());
+            mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
+                    getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
+                    currentRotation);
 
-            final float thumbnailScale;
-            final DeviceProfile profile = mActivity.getDeviceProfile();
-
-            if (getMeasuredWidth() == 0) {
-                // If we haven't measured , skip the thumbnail drawing and only draw the background
-                // color
-                thumbnailScale = 0f;
-            } else {
-                final Configuration configuration =
-                        getContext().getResources().getConfiguration();
-                // Rotate the screenshot if not in multi-window mode
-                isRotated = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
-                        configuration.orientation != mThumbnailData.orientation &&
-                        !mActivity.isInMultiWindowMode() &&
-                        mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
-                // Scale the screenshot to always fit the width of the card.
-                thumbnailScale = isRotated
-                        ? getMeasuredWidth() / thumbnailHeight
-                        : getMeasuredWidth() / thumbnailWidth;
-            }
-
-            if (isRotated) {
-                int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1;
-                mMatrix.setRotate(90 * rotationDir);
-                int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top;
-                int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right;
-                mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
-                if (rotationDir == -1) {
-                    // Crop the right/bottom side of the screenshot rather than left/top
-                    float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight();
-                    mClippedInsets.offset(0, excessHeight);
-                }
-                mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top);
-                // Move the screenshot to the thumbnail window (rotation moved it out).
-                if (rotationDir == 1) {
-                    mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0);
-                } else {
-                    mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth());
-                }
-            } else {
-                mClippedInsets.offsetTo(thumbnailInsets.left * scale, thumbnailInsets.top * scale);
-                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
-            }
-
-            final float widthWithInsets;
-            final float heightWithInsets;
-            if (isRotated) {
-                widthWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-            } else {
-                widthWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-            }
-            mClippedInsets.left *= thumbnailScale;
-            mClippedInsets.top *= thumbnailScale;
-            mClippedInsets.right = widthWithInsets - mClippedInsets.left - getMeasuredWidth();
-            mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - getMeasuredHeight();
-
-            mMatrix.postScale(thumbnailScale, thumbnailScale);
-            mBitmapShader.setLocalMatrix(mMatrix);
-
-            float bitmapHeight = Math.max((isRotated ? thumbnailWidth : thumbnailHeight)
-                    * thumbnailScale, 0);
-            if (Math.round(bitmapHeight) < getMeasuredHeight()) {
-                mClipBottom = bitmapHeight;
-            }
+            mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
             mPaint.setShader(mBitmapShader);
         }
-
-        mRotated = isRotated;
+        getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
         invalidate();
 
         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
@@ -454,4 +445,178 @@
         }
         return mThumbnailData.thumbnail;
     }
+
+    /**
+     * Returns whether the snapshot is real.
+     */
+    public boolean isRealSnapshot() {
+        if (mThumbnailData == null) {
+            return false;
+        }
+        return mThumbnailData.isRealSnapshot;
+    }
+
+    /**
+     * Utility class to position the thumbnail in the TaskView
+     */
+    public static class PreviewPositionHelper {
+
+        // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+        private final RectF mClippedInsets = new RectF();
+        private final Matrix mMatrix = new Matrix();
+        private float mClipBottom = -1;
+        private boolean mIsOrientationChanged;
+
+        public Matrix getMatrix() {
+            return mMatrix;
+        }
+
+        /**
+         * Updates the matrix based on the provided parameters
+         */
+        public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
+                int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
+            boolean isRotated = false;
+            boolean isOrientationDifferent;
+            mClipBottom = -1;
+
+            float scale = thumbnailData.scale;
+            Rect activityInsets = dp.getInsets();
+            Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets);
+            final float thumbnailWidth = thumbnailPosition.width()
+                    - (thumbnailInsets.left + thumbnailInsets.right) * scale;
+            final float thumbnailHeight = thumbnailPosition.height()
+                    - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+
+            final float thumbnailScale;
+            int thumbnailRotation = thumbnailData.rotation;
+            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+
+            // Landscape vs portrait change
+            boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
+                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+            isOrientationDifferent = isOrientationChange(deltaRotate)
+                    && windowingModeSupportsRotation;
+            if (canvasWidth == 0) {
+                // If we haven't measured , skip the thumbnail drawing and only draw the background
+                // color
+                thumbnailScale = 0f;
+            } else {
+                // Rotate the screenshot if not in multi-window mode
+                isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+                // Scale the screenshot to always fit the width of the card.
+                thumbnailScale = isOrientationDifferent
+                        ? canvasWidth / thumbnailHeight
+                        : canvasWidth / thumbnailWidth;
+            }
+
+            Rect splitScreenInsets = dp.getInsets();
+            if (!isRotated) {
+                // No Rotation
+                if (dp.isMultiWindowMode) {
+                    mClippedInsets.offsetTo(splitScreenInsets.left * scale,
+                            splitScreenInsets.top * scale);
+                } else {
+                    mClippedInsets.offsetTo(thumbnailInsets.left * scale,
+                            thumbnailInsets.top * scale);
+                }
+                mMatrix.setTranslate(
+                        -thumbnailInsets.left * scale,
+                        -thumbnailInsets.top * scale);
+            } else {
+                setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
+            }
+
+            final float widthWithInsets;
+            final float heightWithInsets;
+            if (isOrientationDifferent) {
+                widthWithInsets = thumbnailPosition.height() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.width() * thumbnailScale;
+            } else {
+                widthWithInsets = thumbnailPosition.width() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.height() * thumbnailScale;
+            }
+            mClippedInsets.left *= thumbnailScale;
+            mClippedInsets.top *= thumbnailScale;
+
+            if (dp.isMultiWindowMode) {
+                mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
+                mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
+            } else {
+                mClippedInsets.right = Math.max(0,
+                        widthWithInsets - mClippedInsets.left - canvasWidth);
+                mClippedInsets.bottom = Math.max(0,
+                        heightWithInsets - mClippedInsets.top - canvasHeight);
+            }
+
+            mMatrix.postScale(thumbnailScale, thumbnailScale);
+
+            float bitmapHeight = Math.max(0,
+                    (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
+            if (Math.round(bitmapHeight) < canvasHeight) {
+                mClipBottom = bitmapHeight;
+            }
+            mIsOrientationChanged = isOrientationDifferent;
+        }
+
+        private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
+            return new Rect(Math.min(insets.left, activityInsets.left),
+                    Math.min(insets.top, activityInsets.top),
+                    Math.min(insets.right, activityInsets.right),
+                    Math.min(insets.bottom, activityInsets.bottom));
+        }
+
+        private int getRotationDelta(int oldRotation, int newRotation) {
+            int delta = newRotation - oldRotation;
+            if (delta < 0) delta += 4;
+            return delta;
+        }
+
+        /**
+         * @param deltaRotation the number of 90 degree turns from the current orientation
+         * @return {@code true} if the change in rotation results in a shift from landscape to
+         * portrait or vice versa, {@code false} otherwise
+         */
+        private boolean isOrientationChange(int deltaRotation) {
+            return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        }
+
+        private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
+                Rect thumbnailPosition) {
+            int newLeftInset = 0;
+            int newTopInset = 0;
+            int translateX = 0;
+            int translateY = 0;
+
+            mMatrix.setRotate(90 * deltaRotate);
+            switch (deltaRotate) { /* Counter-clockwise */
+                case Surface.ROTATION_90:
+                    newLeftInset = thumbnailInsets.bottom;
+                    newTopInset = thumbnailInsets.left;
+                    translateX = thumbnailPosition.height();
+                    break;
+                case Surface.ROTATION_270:
+                    newLeftInset = thumbnailInsets.top;
+                    newTopInset = thumbnailInsets.right;
+                    translateY = thumbnailPosition.width();
+                    break;
+                case Surface.ROTATION_180:
+                    newLeftInset = -thumbnailInsets.top;
+                    newTopInset = -thumbnailInsets.left;
+                    translateX = thumbnailPosition.width();
+                    translateY = thumbnailPosition.height();
+                    break;
+            }
+            mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
+            mMatrix.postTranslate(translateX - mClippedInsets.left,
+                    translateY - mClippedInsets.top);
+        }
+
+        /**
+         * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
+         */
+        public RectF getInsetsToDrawInFullscreen() {
+            return mClippedInsets;
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 51802df..222f6e6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -16,12 +16,23 @@
 
 package com.android.quickstep.views;
 
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.END;
+import static android.view.Gravity.START;
+import static android.view.Gravity.TOP;
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.Utilities.comp;
 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 android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -30,17 +41,19 @@
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.content.Context;
-import android.content.res.Resources;
+import android.content.Intent;
 import android.graphics.Outline;
 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;
 import android.util.FloatProperty;
 import android.util.Log;
-import android.view.Gravity;
+import android.view.Surface;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -48,26 +61,34 @@
 import android.widget.Toast;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskSystemShortcut;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.TaskCornerRadius;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -105,19 +126,6 @@
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
 
-    public static final FloatProperty<TaskView> FULLSCREEN_PROGRESS =
-            new FloatProperty<TaskView>("fullscreenProgress") {
-                @Override
-                public void setValue(TaskView taskView, float v) {
-                    taskView.setFullscreenProgress(v);
-                }
-
-                @Override
-                public Float get(TaskView taskView) {
-                    return taskView.mFullscreenProgress;
-                }
-            };
-
     private static final FloatProperty<TaskView> FOCUS_TRANSITION =
             new FloatProperty<TaskView>("focusTransition") {
                 @Override
@@ -156,13 +164,12 @@
     private float mCurveScale;
     private float mFullscreenProgress;
     private final FullscreenDrawParams mCurrentFullscreenParams;
-    private final float mCornerRadius;
-    private final float mWindowCornerRadius;
     private final BaseDraggingActivity mActivity;
 
     private ObjectAnimator mIconAndDimAnimator;
     private float mIconScaleAnimStartProgress = 0;
     private float mFocusTransitionProgress = 1;
+    private float mModalness = 0;
     private float mStableAlpha = 1;
 
     private boolean mShowScreenshot;
@@ -173,11 +180,12 @@
 
     // Order in which the footers appear. Lower order appear below higher order.
     public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
-    public static final int INDEX_PROACTIVE_SUGGEST = 1;
     private final FooterWrapper[] mFooters = new FooterWrapper[2];
     private float mFooterVerticalOffset = 0;
     private float mFooterAlpha = 1;
     private int mStackHeight;
+    private View mContextualChipWrapper;
+    private View mContextualChip;
 
     public TaskView(Context context) {
         this(context, null);
@@ -207,18 +215,32 @@
             mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
                     Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
                     TaskUtils.getLaunchComponentKeyForTask(getTask().key));
-            mActivity.getStatsLogManager().logTaskLaunch(getRecentsView(),
-                    TaskUtils.getLaunchComponentKeyForTask(getTask().key));
+            mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
+                    .log(LAUNCHER_TASK_LAUNCH_TAP);
         });
-        mCornerRadius = TaskCornerRadius.get(context);
-        mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
-        mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
 
-        mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams);
+        mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams);
         setOutlineProvider(mOutlineProvider);
     }
 
+    /**
+     * Builds proto for logging
+     */
+    public WorkspaceItemInfo getItemInfo() {
+        ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key);
+        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
+        dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
+        dummyInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
+        dummyInfo.user = componentKey.user;
+        dummyInfo.intent = new Intent().setComponent(componentKey.componentName);
+        dummyInfo.title = TaskUtils.getTitle(getContext(), getTask());
+        dummyInfo.screenId = getRecentsView().indexOfChild(this);
+        return dummyInfo;
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -226,6 +248,26 @@
         mIconView = findViewById(R.id.icon);
     }
 
+    /**
+     * The modalness of this view is how it should be displayed when it is shown on its own in the
+     * modal state of overview.
+     *
+     * @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own.
+     */
+    public void setModalness(float modalness) {
+        mModalness = modalness;
+        mIconView.setAlpha(comp(modalness));
+        if (mContextualChip != null) {
+            mContextualChip.setScaleX(comp(modalness));
+            mContextualChip.setScaleY(comp(modalness));
+        }
+        if (mContextualChipWrapper != null) {
+            mContextualChipWrapper.setAlpha(comp(modalness));
+        }
+
+        updateFooterVerticalOffset(mFooterVerticalOffset);
+    }
+
     public TaskMenuView getMenuView() {
         return mMenuView;
     }
@@ -236,11 +278,15 @@
 
     /**
      * Updates this task view to the given {@param task}.
+     *
+     * TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after
+     *   that issue is fixed
      */
-    public void bind(Task task) {
+    public void bind(Task task, RecentsOrientedState orientedState) {
         cancelPendingLoadTasks();
         mTask = task;
         mSnapshotView.bind(task);
+        setOrientationState(orientedState);
     }
 
     public Task getTask() {
@@ -256,11 +302,9 @@
     }
 
     public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
-        final PendingAnimation pendingAnimation =
-                getRecentsView().createTaskLauncherAnimation(this, RECENTS_LAUNCH_DURATION);
-        pendingAnimation.anim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
-        AnimatorPlaybackController currentAnimation = AnimatorPlaybackController
-                .wrap(pendingAnimation.anim, RECENTS_LAUNCH_DURATION, null);
+        final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation(
+                this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR);
+        AnimatorPlaybackController currentAnimation = pendingAnimation.createPlaybackController();
         currentAnimation.setEndAction(() -> {
             pendingAnimation.finish(true, Touch.SWIPE);
             launchTask(false);
@@ -288,11 +332,19 @@
     public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
             Handler resultCallbackHandler) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            RecentsView recentsView = getRecentsView();
             if (isRunningTask()) {
-                getRecentsView().finishRecentsAnimation(false /* toRecents */,
+                recentsView.finishRecentsAnimation(false /* toRecents */,
                         () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
             } else {
-                launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
+                // This is a workaround against the WM issue that app open is not correctly animated
+                // when recents animation is being cleaned up (b/143774568). When that's possible,
+                // we should rely on the framework side to cancel the recents animation, and we will
+                // clean up the screenshot on the launcher side while we launch the next task.
+                recentsView.switchToScreenshot(null,
+                        () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+                                () -> launchTaskInternal(animate, freezeTaskList, resultCallback,
+                                        resultCallbackHandler)));
             }
         } else {
             launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
@@ -303,6 +355,8 @@
             Consumer<Boolean> resultCallback, Handler resultCallbackHandler) {
         if (mTask != null) {
             final ActivityOptions opts;
+            TestLogging.recordEvent(
+                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
             if (animate) {
                 opts = mActivity.getActivityLaunchOptions(this);
                 if (freezeTaskList) {
@@ -331,6 +385,7 @@
                             }
                         }, resultCallbackHandler);
             }
+            getRecentsView().onTaskLaunched(mTask);
         }
     }
 
@@ -376,12 +431,17 @@
     }
 
     private boolean showTaskMenu(int action) {
-        getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
-        mMenuView = TaskMenuView.showForTask(this);
-        UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE,
-                LauncherLogProto.ItemType.TASK_ICON);
-        if (mMenuView != null) {
-            mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
+        if (!getRecentsView().isClearAllHidden()) {
+            getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
+        } else {
+            mMenuView = TaskMenuView.showForTask(this);
+            mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
+                    .log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS);
+            UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE,
+                    LauncherLogProto.ItemType.TASK_ICON);
+            if (mMenuView != null) {
+                mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
+            }
         }
         return mMenuView != null;
     }
@@ -401,6 +461,44 @@
         }
     }
 
+    public void setOrientationState(RecentsOrientedState orientationState) {
+        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
+        int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+        LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
+        switch (orientationHandler.getRotation()) {
+            case Surface.ROTATION_90:
+                iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
+                iconParams.rightMargin = -thumbnailPadding;
+                iconParams.leftMargin = 0;
+                iconParams.topMargin = snapshotParams.topMargin / 2;
+                break;
+            case Surface.ROTATION_180:
+                iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+                iconParams.bottomMargin = -thumbnailPadding;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                break;
+            case Surface.ROTATION_270:
+                iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+                iconParams.leftMargin = -thumbnailPadding;
+                iconParams.rightMargin = 0;
+                iconParams.topMargin = snapshotParams.topMargin / 2;
+                break;
+            case Surface.ROTATION_0:
+            default:
+                iconParams.gravity = TOP | CENTER_HORIZONTAL;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                break;
+        }
+        mIconView.setLayoutParams(iconParams);
+        mIconView.setRotation(orientationHandler.getDegreesRotated());
+
+        if (mMenuView != null) {
+            mMenuView.onRotationChanged();
+        }
+    }
+
     private void setIconAndDimTransitionProgress(float progress, boolean invert) {
         if (invert) {
             progress = 1 - progress;
@@ -415,12 +513,7 @@
         mIconView.setScaleX(scale);
         mIconView.setScaleY(scale);
 
-        mFooterVerticalOffset = 1.0f - scale;
-        for (FooterWrapper footer : mFooters) {
-            if (footer != null) {
-                footer.updateFooterOffset();
-            }
-        }
+        updateFooterVerticalOffset(1.0f - scale);
     }
 
     public void setIconScaleAnimStartProgress(float startProgress) {
@@ -454,7 +547,7 @@
         setIconAndDimTransitionProgress(iconScale, invert);
     }
 
-    private void resetViewTransforms() {
+    protected void resetViewTransforms() {
         setCurveScale(1);
         setTranslationX(0f);
         setTranslationY(0f);
@@ -463,11 +556,6 @@
         setIconScaleAndDim(1);
     }
 
-    public void resetVisualProperties() {
-        resetViewTransforms();
-        setFullscreenProgress(0);
-    }
-
     public void setStableAlpha(float parentAlpha) {
         mStableAlpha = parentAlpha;
         setAlpha(mStableAlpha);
@@ -485,11 +573,17 @@
 
     @Override
     public void onPageScroll(ScrollState scrollState) {
+        // Don't do anything if it's modal.
+        if (mModalness > 0) {
+            return;
+        }
+
         float curveInterpolation =
                 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
-
+        float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation(
+                curveInterpolation);
         mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
-        setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
+        setCurveScale(curveScaleForCurveInterpolation);
 
         mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
         for (FooterWrapper footer : mFooters) {
@@ -499,13 +593,15 @@
         }
 
         if (mMenuView != null) {
-            mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
+            PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler();
+            RecentsView recentsView = getRecentsView();
+            mMenuView.setPosition(getX() - recentsView.getScrollX(),
+                    getY() - recentsView.getScrollY(), pagedOrientationHandler);
             mMenuView.setScaleX(getScaleX());
             mMenuView.setScaleY(getScaleY());
         }
     }
 
-
     /**
      * Sets the footer at the specific index and returns the previously set footer.
      */
@@ -533,8 +629,10 @@
             }
 
             addView(view, indexToAdd);
-            ((LayoutParams) view.getLayoutParams()).gravity =
-                    Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+            LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+            layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+            layoutParams.bottomMargin =
+                    ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin;
             view.setAlpha(mFooterAlpha);
             mFooters[index] = new FooterWrapper(view);
             if (shouldAnimateEntry) {
@@ -555,6 +653,68 @@
         return oldFooter;
     }
 
+    /**
+     * Sets the contextual chip.
+     *
+     * @param view Wrapper view containing contextual chip.
+     */
+    public void setContextualChip(View view) {
+        if (mContextualChipWrapper != null) {
+            removeView(mContextualChipWrapper);
+        }
+        if (view != null) {
+            mContextualChipWrapper = view;
+            LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.WRAP_CONTENT);
+            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);
+            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) {
+                mContextualChipWrapper.animate().alpha(1f).setDuration(50);
+            }
+        }
+    }
+
+    public float getTaskCornerRadius() {
+        return TaskCornerRadius.get(mActivity);
+    }
+
+    /**
+     * Clears the contextual chip from TaskView.
+     *
+     * @return The contextual chip wrapper view to be recycled.
+     */
+    public View clearContextualChip() {
+        if (mContextualChipWrapper != null) {
+            removeView(mContextualChipWrapper);
+        }
+        View oldContextualChipWrapper = mContextualChipWrapper;
+        mContextualChipWrapper = null;
+        mContextualChip = null;
+        return oldContextualChipWrapper;
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
@@ -571,6 +731,12 @@
                 mStackHeight += footer.mView.getHeight();
             }
         }
+        updateFooterVerticalOffset(0);
+    }
+
+    private void updateFooterVerticalOffset(float offset) {
+        mFooterVerticalOffset = offset;
+
         for (FooterWrapper footer : mFooters) {
             if (footer != null) {
                 footer.updateFooterOffset();
@@ -587,21 +753,16 @@
         return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
     }
 
-    public void setCurveScale(float curveScale) {
+    private void setCurveScale(float curveScale) {
         mCurveScale = curveScale;
-        onScaleChanged();
+        setScaleX(mCurveScale);
+        setScaleY(mCurveScale);
     }
 
     public float getCurveScale() {
         return mCurveScale;
     }
 
-    private void onScaleChanged() {
-        float scale = mCurveScale;
-        setScaleX(scale);
-        setScaleY(scale);
-    }
-
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -613,8 +774,9 @@
         private final int mMarginTop;
         private FullscreenDrawParams mFullscreenParams;
 
-        TaskOutlineProvider(Resources res, FullscreenDrawParams fullscreenParams) {
-            mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
+            mMarginTop = context.getResources().getDimensionPixelSize(
+                    R.dimen.task_thumbnail_top_margin);
             mFullscreenParams = fullscreenParams;
         }
 
@@ -652,14 +814,7 @@
             mDelegate = mOldOutlineProvider == null
                     ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;
 
-            int h = view.getLayoutParams().height;
-            if (h > 0) {
-                mExpectedHeight = h;
-            } else {
-                int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
-                view.measure(m, m);
-                mExpectedHeight = view.getMeasuredHeight();
-            }
+            mExpectedHeight = getExpectedViewHeight(view);
             mOldPaddingBottom = view.getPaddingBottom();
 
             if (mOldOutlineProvider != null) {
@@ -680,7 +835,8 @@
         }
 
         void updateFooterOffset() {
-            mAnimationOffset = Math.round(mStackHeight * mFooterVerticalOffset);
+            float offset = Utilities.or(mFooterVerticalOffset, mModalness);
+            mAnimationOffset = Math.round(mStackHeight * offset);
             mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset
                     + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom
                     + mCurrentFullscreenParams.mCurrentDrawnInsets.top);
@@ -705,24 +861,30 @@
         }
     }
 
+    private int getExpectedViewHeight(View view) {
+        int expectedHeight;
+        int h = view.getLayoutParams().height;
+        if (h > 0) {
+            expectedHeight = h;
+        } else {
+            int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
+            view.measure(m, m);
+            expectedHeight = view.getMeasuredHeight();
+        }
+        return expectedHeight;
+    }
+
     @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
 
         info.addAction(
-                new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task,
-                        getContext().getText(R.string.accessibility_close_task)));
+                new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close,
+                        getContext().getText(R.string.accessibility_close)));
 
         final Context context = getContext();
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this);
-            if (onClickListener != null) {
-                info.addAction(menuOption.createAccessibilityAction(context));
-            }
+        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+            info.addAction(s.createAccessibilityAction(context));
         }
 
         if (mDigitalWellBeingToast.hasLimit()) {
@@ -735,14 +897,14 @@
         final RecentsView recentsView = getRecentsView();
         final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
                 AccessibilityNodeInfo.CollectionItemInfo.obtain(
-                        0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
-                        false);
+                        0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
+                        1, false);
         info.setCollectionItemInfo(itemInfo);
     }
 
     @Override
     public boolean performAccessibilityAction(int action, Bundle arguments) {
-        if (action == R.string.accessibility_close_task) {
+        if (action == R.string.accessibility_close) {
             getRecentsView().dismissTask(this, true /*animateTaskView*/,
                     true /*removeTask*/);
             return true;
@@ -753,16 +915,9 @@
             return true;
         }
 
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            if (menuOption.hasHandlerForAction(action)) {
-                OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this);
-                if (onClickListener != null) {
-                    onClickListener.onClick(this);
-                }
+        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+            if (s.hasHandlerForAction(action)) {
+                s.onClick(this);
                 return true;
             }
         }
@@ -774,6 +929,10 @@
         return (RecentsView) getParent();
     }
 
+    PagedOrientationHandler getPagedOrientationHandler() {
+        return getRecentsView().mOrientationState.getOrientationHandler();
+    }
+
     public void notifyTaskLaunchFailed(String tag) {
         String msg = "Failed to launch task";
         if (mTask != null) {
@@ -785,13 +944,11 @@
 
     /**
      * Hides the icon and shows insets when this TaskView is about to be shown fullscreen.
+     *
      * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
      */
     public void setFullscreenProgress(float progress) {
         progress = Utilities.boundToRange(progress, 0, 1);
-        if (progress == mFullscreenProgress) {
-            return;
-        }
         mFullscreenProgress = progress;
         boolean isFullscreen = mFullscreenProgress > 0;
         mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
@@ -799,23 +956,7 @@
         setClipToPadding(!isFullscreen);
 
         TaskThumbnailView thumbnail = getThumbnail();
-        boolean isMultiWindowMode = mActivity.getDeviceProfile().isMultiWindowMode;
-        RectF insets = thumbnail.getInsetsToDrawInFullscreen(isMultiWindowMode);
-        float currentInsetsLeft = insets.left * mFullscreenProgress;
-        float currentInsetsRight = insets.right * mFullscreenProgress;
-        mCurrentFullscreenParams.setInsets(currentInsetsLeft,
-                insets.top * mFullscreenProgress,
-                currentInsetsRight,
-                insets.bottom * mFullscreenProgress);
-        float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius;
-        mCurrentFullscreenParams.setCornerRadius(Utilities.mapRange(mFullscreenProgress,
-                mCornerRadius, fullscreenCornerRadius) / getRecentsView().getScaleX());
-        // We scaled the thumbnail to fit the content (excluding insets) within task view width.
-        // Now that we are drawing left/right insets again, we need to scale down to fit them.
-        if (getWidth() > 0) {
-            mCurrentFullscreenParams.setScale(getWidth()
-                    / (getWidth() + currentInsetsLeft + currentInsetsRight));
-        }
+        updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
 
         if (!getRecentsView().isTaskIconScaledDown(this)) {
             // Some of the items in here are dependent on the current fullscreen params, but don't
@@ -828,6 +969,17 @@
         invalidateOutline();
     }
 
+    void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
+        if (getRecentsView() == null) {
+            return;
+        }
+        mCurrentFullscreenParams.setProgress(
+                mFullscreenProgress,
+                getRecentsView().getScaleX(),
+                getWidth(), mActivity.getDeviceProfile(),
+                previewPositionHelper);
+    }
+
     public boolean isRunningTask() {
         if (getRecentsView() == null) {
             return false;
@@ -853,26 +1005,46 @@
     /**
      * We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
      */
-    static class FullscreenDrawParams {
-        RectF mCurrentDrawnInsets = new RectF();
-        float mCurrentDrawnCornerRadius;
+    public static class FullscreenDrawParams {
+
+        private final float mCornerRadius;
+        private final float mWindowCornerRadius;
+
+        public RectF mCurrentDrawnInsets = new RectF();
+        public float mCurrentDrawnCornerRadius;
         /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
-        float mScale = 1;
+        public float mScale = 1;
 
-        public FullscreenDrawParams(float cornerRadius) {
-            setCornerRadius(cornerRadius);
+        public FullscreenDrawParams(Context context) {
+            mCornerRadius = TaskCornerRadius.get(context);
+            mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
+
+            mCurrentDrawnCornerRadius = mCornerRadius;
         }
 
-        public void setInsets(float left, float top, float right, float bottom) {
-            mCurrentDrawnInsets.set(left, top, right, bottom);
+        /**
+         * Sets the progress in range [0, 1]
+         */
+        public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
+                DeviceProfile dp, PreviewPositionHelper pph) {
+            RectF insets = pph.getInsetsToDrawInFullscreen();
+
+            float currentInsetsLeft = insets.left * fullscreenProgress;
+            float currentInsetsRight = insets.right * fullscreenProgress;
+            mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
+                    currentInsetsRight, insets.bottom * fullscreenProgress);
+            float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius;
+
+            mCurrentDrawnCornerRadius =
+                    Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
+                            / parentScale;
+
+            // We scaled the thumbnail to fit the content (excluding insets) within task view width.
+            // Now that we are drawing left/right insets again, we need to scale down to fit them.
+            if (previewWidth > 0) {
+                mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
+            }
         }
 
-        public void setCornerRadius(float cornerRadius) {
-            mCurrentDrawnCornerRadius = cornerRadius;
-        }
-
-        public void setScale(float scale) {
-            mScale = scale;
-        }
     }
 }
diff --git a/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..57423c2
--- /dev/null
+++ b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="?android:attr/dialogCornerRadius"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/assistant_gesture.xml b/quickstep/res/drawable/assistant_gesture.xml
new file mode 100644
index 0000000..ba4331c
--- /dev/null
+++ b/quickstep/res/drawable/assistant_gesture.xml
@@ -0,0 +1,989 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="412dp"
+            android:height="890dp"
+            android:viewportWidth="412"
+            android:viewportHeight="890">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_4_G_N_2_N_3_N_4_T_1"
+                    android:rotation="-10"
+                    android:translateX="661.757"
+                    android:translateY="1026.235">
+                    <group
+                        android:name="_R_G_L_4_G_N_2_N_3_N_4_T_0"
+                        android:translateX="-148.438"
+                        android:translateY="-239.65">
+                        <group
+                            android:name="_R_G_L_4_G_N_2_N_3_T_0"
+                            android:translateX="-61.73500000000001"
+                            android:translateY="38.257000000000005">
+                            <group
+                                android:name="_R_G_L_4_G_N_2_T_0"
+                                android:pivotX="83.124"
+                                android:pivotY="89.259"
+                                android:rotation="-16"
+                                android:translateX="-50.44799999999999"
+                                android:translateY="-62.925">
+                                <group
+                                    android:name="_R_G_L_4_G"
+                                    android:translateX="-18.21"
+                                    android:translateY="-17.394">
+                                    <path
+                                        android:name="_R_G_L_4_G_D_0_P_0"
+                                        android:fillAlpha="0"
+                                        android:fillColor="#e8f0fe"
+                                        android:fillType="nonZero"
+                                        android:pathData=" M96.25 48.25 C96.25,74.76 74.76,96.25 48.25,96.25 C21.74,96.25 0.25,74.76 0.25,48.25 C0.25,21.74 21.74,0.25 48.25,0.25 C74.76,0.25 96.25,21.74 96.25,48.25c " />
+                                </group>
+                            </group>
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_3_G"
+                    android:pivotX="48.25"
+                    android:pivotY="48.25"
+                    android:rotation="11"
+                    android:translateX="227.046"
+                    android:translateY="642.467">
+                    <path
+                        android:name="_R_G_L_3_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#e8f0fe"
+                        android:fillType="nonZero"
+                        android:pathData=" M96.25 48.25 C96.25,74.76 74.76,96.25 48.25,96.25 C21.74,96.25 0.25,74.76 0.25,48.25 C0.25,21.74 21.74,0.25 48.25,0.25 C74.76,0.25 96.25,21.74 96.25,48.25c " />
+                </group>
+                <group
+                    android:name="_R_G_L_2_G_T_1"
+                    android:rotation="-10"
+                    android:translateX="661.757"
+                    android:translateY="1026.235">
+                    <group
+                        android:name="_R_G_L_2_G"
+                        android:translateX="-148.438"
+                        android:translateY="-239.65">
+                        <path
+                            android:name="_R_G_L_2_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#d2e3fc"
+                            android:fillType="nonZero"
+                            android:pathData=" M14.25 224.54 C40.46,320.73 128.6,415.05 212.61,415.05 C296.63,415.05 188.65,199.66 188.65,99.96 C188.65,0.25 122.97,160.22 18.3,95.88 C1.02,85.21 0.25,173.17 14.25,224.54c " />
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_1_G_N_4_T_1"
+                    android:rotation="-10"
+                    android:translateX="661.757"
+                    android:translateY="1026.235">
+                    <group
+                        android:name="_R_G_L_1_G_N_4_T_0"
+                        android:translateX="-148.438"
+                        android:translateY="-239.65">
+                        <group
+                            android:name="_R_G_L_1_G"
+                            android:translateX="-61.73500000000001"
+                            android:translateY="38.257000000000005">
+                            <path
+                                android:name="_R_G_L_1_G_D_0_P_0"
+                                android:fillAlpha="1"
+                                android:fillColor="#d2e3fc"
+                                android:fillType="nonZero"
+                                android:pathData=" M60.8 0.25 C60.8,0.25 76.32,20.36 105.56,60.3 C134.8,100.25 207.8,82.25 207.8,82.25 C207.8,82.25 211.01,150.18 211.01,150.18 C211.01,150.18 81.43,193.5 81.43,193.5 C81.43,193.5 31.8,174.25 31.8,174.25 C31.8,174.25 27.8,109.25 22.8,89.25 C17.8,69.25 6.15,49.9 2.17,41.17 C0.25,36.95 13.88,27.25 28.48,18.35 C44.09,8.84 60.8,0.25 60.8,0.25c " />
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_N_3_N_4_T_1"
+                    android:rotation="-10"
+                    android:translateX="661.757"
+                    android:translateY="1026.235">
+                    <group
+                        android:name="_R_G_L_0_G_N_3_N_4_T_0"
+                        android:translateX="-148.438"
+                        android:translateY="-239.65">
+                        <group
+                            android:name="_R_G_L_0_G_N_3_T_0"
+                            android:translateX="-61.73500000000001"
+                            android:translateY="38.257000000000005">
+                            <group
+                                android:name="_R_G_L_0_G"
+                                android:pivotX="83.124"
+                                android:pivotY="89.259"
+                                android:rotation="-16"
+                                android:translateX="-50.44799999999999"
+                                android:translateY="-62.925">
+                                <path
+                                    android:name="_R_G_L_0_G_D_0_P_0"
+                                    android:fillAlpha="1"
+                                    android:fillColor="#d2e3fc"
+                                    android:fillType="nonZero"
+                                    android:pathData=" M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 76.45,102.74 64.42,104.51 C52.38,106.27 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c " />
+                                <path
+                                    android:name="_R_G_L_0_G_D_1_P_0"
+                                    android:pathData=" M57.29 26.52 C59.8,29.86 61.05,33.88 55.15,38.41 C55.15,38.41 49.91,42.03 36.73,51.93 C34.96,53.26 32.72,55.37 30.64,55.46 C28.92,55.55 26.91,54.65 25.14,51.9 C19.1,42.49 15,36.09 17.17,30.02 "
+                                    android:strokeWidth="6"
+                                    android:strokeAlpha="1"
+                                    android:strokeColor="#a0c2f9" />
+                                <path
+                                    android:name="_R_G_L_0_G_D_2_P_0"
+                                    android:pathData=" M64.41 90.04 C64.41,90.04 69.67,62.88 99.15,59.26 "
+                                    android:strokeWidth="6"
+                                    android:strokeAlpha="1"
+                                    android:strokeColor="#a0c2f9" />
+                            </group>
+                        </group>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_4_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="233"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="333"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_4_G_N_2_T_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-16"
+                    android:valueTo="-16"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="783"
+                    android:propertyName="rotation"
+                    android:startOffset="450"
+                    android:valueFrom="-16"
+                    android:valueTo="11"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="11"
+                    android:valueTo="11"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="11"
+                    android:valueTo="-16"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_4_G_N_2_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="661.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="661.757"
+                    android:valueTo="493.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateX"
+                    android:startOffset="1233"
+                    android:valueFrom="493.757"
+                    android:valueTo="497.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateX"
+                    android:startOffset="1683"
+                    android:valueFrom="497.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_4_G_N_2_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="1026.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="1026.235"
+                    android:valueTo="933.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateY"
+                    android:startOffset="1233"
+                    android:valueFrom="933.235"
+                    android:valueTo="939.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateY"
+                    android:startOffset="1683"
+                    android:valueFrom="939.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_4_G_N_2_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-10"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-10"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="0"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_4_G_N_2_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleY"
+                    android:startOffset="1233"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="383"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="1233"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleX"
+                    android:startOffset="1233"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="661.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="661.757"
+                    android:valueTo="493.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateX"
+                    android:startOffset="1233"
+                    android:valueFrom="493.757"
+                    android:valueTo="497.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateX"
+                    android:startOffset="1683"
+                    android:valueFrom="497.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="1026.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="1026.235"
+                    android:valueTo="933.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateY"
+                    android:startOffset="1233"
+                    android:valueFrom="933.235"
+                    android:valueTo="939.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateY"
+                    android:startOffset="1683"
+                    android:valueFrom="939.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-10"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-10"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="0"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="661.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="661.757"
+                    android:valueTo="493.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateX"
+                    android:startOffset="1233"
+                    android:valueFrom="493.757"
+                    android:valueTo="497.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateX"
+                    android:startOffset="1683"
+                    android:valueFrom="497.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="1026.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="1026.235"
+                    android:valueTo="933.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateY"
+                    android:startOffset="1233"
+                    android:valueFrom="933.235"
+                    android:valueTo="939.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateY"
+                    android:startOffset="1683"
+                    android:valueFrom="939.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-10"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-10"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="0"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 76.45,102.74 64.42,104.51 C52.38,106.27 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueTo="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 76.45,102.74 64.42,104.51 C52.38,106.27 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="783"
+                    android:propertyName="pathData"
+                    android:startOffset="450"
+                    android:valueFrom="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 76.45,102.74 64.42,104.51 C52.38,106.27 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueTo="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 103.84,114.98 73.16,119.19 C61.1,120.84 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="pathData"
+                    android:startOffset="1233"
+                    android:valueFrom="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 103.84,114.98 73.16,119.19 C61.1,120.84 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueTo="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 103.84,114.98 73.16,119.19 C61.1,120.84 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="pathData"
+                    android:startOffset="1683"
+                    android:valueFrom="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 103.84,114.98 73.16,119.19 C61.1,120.84 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueTo="M19.19 14.59 C40.48,0.25 55.28,10.31 73.92,28.68 C92.56,47.05 106.44,53.33 112.22,64.44 C118,75.56 76.45,102.74 64.42,104.51 C52.38,106.27 30.98,101.06 19.76,76.77 C8.07,51.48 0.54,27.15 19.19,14.59c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-16"
+                    android:valueTo="-16"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="783"
+                    android:propertyName="rotation"
+                    android:startOffset="450"
+                    android:valueFrom="-16"
+                    android:valueTo="11"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="11"
+                    android:valueTo="11"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="11"
+                    android:valueTo="-16"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="661.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="661.757"
+                    android:valueTo="493.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateX"
+                    android:startOffset="1233"
+                    android:valueFrom="493.757"
+                    android:valueTo="497.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateX"
+                    android:startOffset="1683"
+                    android:valueFrom="497.757"
+                    android:valueTo="661.757"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="1026.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="1026.235"
+                    android:valueTo="933.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="translateY"
+                    android:startOffset="1233"
+                    android:valueFrom="933.235"
+                    android:valueTo="939.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="translateY"
+                    android:startOffset="1683"
+                    android:valueFrom="939.235"
+                    android:valueTo="1026.235"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.303,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_3_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-10"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="900"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-10"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="rotation"
+                    android:startOffset="1233"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1400"
+                    android:propertyName="rotation"
+                    android:startOffset="1683"
+                    android:valueFrom="0"
+                    android:valueTo="-10"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.205,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="3167"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture.xml b/quickstep/res/drawable/back_gesture.xml
new file mode 100644
index 0000000..18ad2cb
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture.xml
@@ -0,0 +1,898 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="412dp"
+            android:height="890dp"
+            android:viewportWidth="412"
+            android:viewportHeight="890">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_3_G_T_1"
+                    android:rotation="29"
+                    android:translateX="400.931"
+                    android:translateY="449.112">
+                    <group
+                        android:name="_R_G_L_3_G"
+                        android:translateX="-51.449"
+                        android:translateY="-51.449">
+                        <path
+                            android:name="_R_G_L_3_G_D_0_P_0"
+                            android:fillAlpha="0"
+                            android:fillColor="#e8f0fe"
+                            android:fillType="nonZero"
+                            android:pathData=" M98.99 58.07 C95.33,84.33 71.08,102.65 44.83,98.99 C18.57,95.33 0.25,71.08 3.91,44.83 C7.57,18.57 31.82,0.25 58.07,3.91 C84.33,7.57 102.65,31.82 98.99,58.07c " />
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_2_G_T_1"
+                    android:rotation="22"
+                    android:translateX="443.275"
+                    android:translateY="741.789">
+                    <group
+                        android:name="_R_G_L_2_G"
+                        android:translateX="-199.299"
+                        android:translateY="-310.469">
+                        <path
+                            android:name="_R_G_L_2_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#d2e3fc"
+                            android:fillType="nonZero"
+                            android:pathData=" M98.53 346.74 C150.31,507.99 279.63,534.69 366.99,534.69 C454.35,534.69 342.13,310.99 342.13,207.18 C342.13,103.37 244.29,336.77 102.92,212.59 C84.64,201.99 83.91,293.37 98.53,346.74c " />
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_1_G_N_2_N_4_T_1"
+                    android:rotation="22"
+                    android:translateX="443.275"
+                    android:translateY="741.789">
+                    <group
+                        android:name="_R_G_L_1_G_N_2_N_4_T_0"
+                        android:translateX="-199.299"
+                        android:translateY="-310.469">
+                        <group
+                            android:name="_R_G_L_1_G_N_2_T_0"
+                            android:translateX="32.843"
+                            android:translateY="70.37599999999998">
+                            <group
+                                android:name="_R_G_L_1_G"
+                                android:pivotX="63.901"
+                                android:pivotY="99.512"
+                                android:rotation="-7"
+                                android:translateX="-28.42"
+                                android:translateY="-66.62700000000001">
+                                <path
+                                    android:name="_R_G_L_1_G_D_0_P_0"
+                                    android:fillAlpha="1"
+                                    android:fillColor="#d2e3fc"
+                                    android:fillType="nonZero"
+                                    android:pathData=" M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c " />
+                                <path
+                                    android:name="_R_G_L_1_G_D_1_P_0"
+                                    android:pathData=" M62.63 26.42 C64.51,30.16 64.56,33.23 61.29,34.92 C61.29,34.92 57.3,38.02 54.34,39.55 C52.36,40.56 31.95,52.96 29.89,52.69 C28.18,52.46 25.13,43.84 23.56,39.57 C19.67,28.95 23.51,23 30.85,17.22 "
+                                    android:strokeWidth="6"
+                                    android:strokeAlpha="1"
+                                    android:strokeColor="#a0c2f9" />
+                            </group>
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_N_4_T_1"
+                    android:rotation="22"
+                    android:translateX="443.275"
+                    android:translateY="741.789">
+                    <group
+                        android:name="_R_G_L_0_G_N_4_T_0"
+                        android:translateX="-199.299"
+                        android:translateY="-310.469">
+                        <group
+                            android:name="_R_G_L_0_G"
+                            android:translateX="32.843"
+                            android:translateY="70.37599999999998">
+                            <path
+                                android:name="_R_G_L_0_G_D_0_P_0"
+                                android:fillAlpha="1"
+                                android:fillColor="#d2e3fc"
+                                android:fillType="nonZero"
+                                android:pathData=" M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 36.96,112.53 21.91,82.02 C12.91,63.77 2.48,47.19 -0.01,35.78 C-0.24,33.88 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c " />
+                            <path
+                                android:name="_R_G_L_0_G_D_1_P_0"
+                                android:pathData=" M37.4 46.68 C37.4,46.68 53.75,25.93 70.25,28.88 "
+                                android:strokeWidth="6"
+                                android:strokeAlpha="1"
+                                android:strokeColor="#a0c2f9" />
+                            <path
+                                android:name="_R_G_L_0_G_D_2_P_0"
+                                android:pathData=" M157.71 101.59 C157.71,101.59 165.04,114.63 190.3,113 "
+                                android:strokeWidth="6"
+                                android:strokeAlpha="1"
+                                android:strokeColor="#a0c2f9" />
+                        </group>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_3_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="333"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="500"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="367"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="1000"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="400.931"
+                    android:valueTo="400.931"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="400.931"
+                    android:valueTo="232.931"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="443.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="443.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateX"
+                    android:startOffset="1000"
+                    android:valueFrom="403.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateX"
+                    android:startOffset="1600"
+                    android:valueFrom="403.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1000"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateY"
+                    android:startOffset="1600"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="22"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="22"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1000"
+                    android:valueFrom="0"
+                    android:valueTo="2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.324,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="rotation"
+                    android:startOffset="1600"
+                    android:valueFrom="2"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1000"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueTo="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="pathData"
+                    android:startOffset="1000"
+                    android:valueFrom="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueTo="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="pathData"
+                    android:startOffset="1600"
+                    android:valueFrom="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueTo="M24.09 7.19 C24.09,7.19 24.09,7.19 24.09,7.19 C37.34,0.25 53.74,5.21 61.53,18.2 C61.53,18.2 92.06,67.99 92.06,67.99 C99.55,85.1 94.55,109.83 77.93,118.03 C77.93,118.03 73.41,119.96 73.41,119.96 C54.93,128.77 33.79,118.95 27.61,98.93 C27.61,98.93 9.41,43.77 9.41,43.77 C4.59,29.86 10.87,13.76 24.09,7.19c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M62.63 26.42 C64.51,30.16 64.56,33.23 61.29,34.92 C61.29,34.92 57.3,38.02 54.34,39.55 C52.36,40.56 31.95,52.96 29.89,52.69 C28.18,52.46 25.13,43.84 23.56,39.57 C19.67,28.95 23.51,23 30.85,17.22 "
+                    android:valueTo="M62.63 26.42 C64.51,30.16 64.56,33.23 61.29,34.92 C61.29,34.92 57.3,38.02 54.34,39.55 C52.36,40.56 31.95,52.96 29.89,52.69 C28.18,52.46 25.13,43.84 23.56,39.57 C19.67,28.95 23.51,23 30.85,17.22 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M62.63 26.42 C64.51,30.16 64.56,33.23 61.29,34.92 C61.29,34.92 57.3,38.02 54.34,39.55 C52.36,40.56 31.95,52.96 29.89,52.69 C28.18,52.46 25.13,43.84 23.56,39.57 C19.67,28.95 23.51,23 30.85,17.22 "
+                    android:valueTo="M58.31 29.65 C60.19,33.38 60.72,37.56 54.12,40.99 C54.12,40.99 48.32,43.62 33.61,51.06 C31.63,52.06 29.06,53.73 26.99,53.46 C25.28,53.24 23.46,52.01 22.2,48.99 C17.92,38.66 15,31.64 18.2,26.05 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="pathData"
+                    android:startOffset="1000"
+                    android:valueFrom="M58.31 29.65 C60.19,33.38 60.72,37.56 54.12,40.99 C54.12,40.99 48.32,43.62 33.61,51.06 C31.63,52.06 29.06,53.73 26.99,53.46 C25.28,53.24 23.46,52.01 22.2,48.99 C17.92,38.66 15,31.64 18.2,26.05 "
+                    android:valueTo="M58.31 29.65 C60.19,33.38 60.72,37.56 54.12,40.99 C54.12,40.99 48.32,43.62 33.61,51.06 C31.63,52.06 29.06,53.73 26.99,53.46 C25.28,53.24 23.46,52.01 22.2,48.99 C17.92,38.66 15,31.64 18.2,26.05 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="pathData"
+                    android:startOffset="1600"
+                    android:valueFrom="M58.31 29.65 C60.19,33.38 60.72,37.56 54.12,40.99 C54.12,40.99 48.32,43.62 33.61,51.06 C31.63,52.06 29.06,53.73 26.99,53.46 C25.28,53.24 23.46,52.01 22.2,48.99 C17.92,38.66 15,31.64 18.2,26.05 "
+                    android:valueTo="M62.63 26.42 C64.51,30.16 64.56,33.23 61.29,34.92 C61.29,34.92 57.3,38.02 54.34,39.55 C52.36,40.56 31.95,52.96 29.89,52.69 C28.18,52.46 25.13,43.84 23.56,39.57 C19.67,28.95 23.51,23 30.85,17.22 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-7"
+                    android:valueTo="-7"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.315,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-7"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.315,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1000"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="rotation"
+                    android:startOffset="1600"
+                    android:valueFrom="0"
+                    android:valueTo="-7"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_2_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="443.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="443.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateX"
+                    android:startOffset="1000"
+                    android:valueFrom="403.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateX"
+                    android:startOffset="1600"
+                    android:valueFrom="403.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_2_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1000"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateY"
+                    android:startOffset="1600"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_2_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="22"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="22"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1000"
+                    android:valueFrom="0"
+                    android:valueTo="2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.324,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="rotation"
+                    android:startOffset="1600"
+                    android:valueFrom="2"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 36.96,112.53 21.91,82.02 C12.91,63.77 2.48,47.19 -0.01,35.78 C-0.24,33.88 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueTo="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 36.96,112.53 21.91,82.02 C12.91,63.77 2.48,47.19 -0.01,35.78 C-0.24,33.88 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 36.96,112.53 21.91,82.02 C12.91,63.77 2.48,47.19 -0.01,35.78 C-0.24,33.88 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueTo="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 39,110.89 30.58,91.46 C22.47,72.8 10.93,51.25 2.97,40.51 C2.75,38.61 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="pathData"
+                    android:startOffset="1000"
+                    android:valueFrom="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 39,110.89 30.58,91.46 C22.47,72.8 10.93,51.25 2.97,40.51 C2.75,38.61 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueTo="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 39,110.89 30.58,91.46 C22.47,72.8 10.93,51.25 2.97,40.51 C2.75,38.61 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="pathData"
+                    android:startOffset="1600"
+                    android:valueFrom="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 39,110.89 30.58,91.46 C22.47,72.8 10.93,51.25 2.97,40.51 C2.75,38.61 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueTo="M46.18 2.25 C46.18,2.25 63.07,0.46 63.07,0.46 C71.25,13.11 74.51,28.4 84.08,41.9 C107.62,73.73 129.25,98.04 175.54,101.59 C278.31,96.7 179.52,210.03 162.67,266.4 C162.67,266.4 58.91,211.6 58.91,211.6 C56.64,197.88 36.96,112.53 21.91,82.02 C12.91,63.77 2.48,47.19 -0.01,35.78 C-0.24,33.88 0.25,27.89 12.82,15.25 C21.14,6.07 34.31,0.25 46.18,2.25c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.269,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M37.4 46.68 C37.4,46.68 53.75,25.93 70.25,28.88 "
+                    android:valueTo="M37.4 46.68 C37.4,46.68 53.75,25.93 70.25,28.88 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M37.4 46.68 C37.4,46.68 53.75,25.93 70.25,28.88 "
+                    android:valueTo="M29.25 53.2 C29.25,53.2 40.54,27.94 70.07,31.11 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="pathData"
+                    android:startOffset="1000"
+                    android:valueFrom="M29.25 53.2 C29.25,53.2 40.54,27.94 70.07,31.11 "
+                    android:valueTo="M29.25 53.2 C29.25,53.2 40.54,27.94 70.07,31.11 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="pathData"
+                    android:startOffset="1600"
+                    android:valueFrom="M29.25 53.2 C29.25,53.2 40.54,27.94 70.07,31.11 "
+                    android:valueTo="M37.4 46.68 C37.4,46.68 53.75,25.93 70.25,28.88 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.269,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="443.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="443.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateX"
+                    android:startOffset="1000"
+                    android:valueFrom="403.275"
+                    android:valueTo="403.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateX"
+                    android:startOffset="1600"
+                    android:valueFrom="403.275"
+                    android:valueTo="443.275"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1000"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="translateY"
+                    android:startOffset="1600"
+                    android:valueFrom="741.789"
+                    android:valueTo="741.789"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_N_4_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="22"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="667"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="22"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1000"
+                    android:valueFrom="0"
+                    android:valueTo="2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.324,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1067"
+                    android:propertyName="rotation"
+                    android:startOffset="1600"
+                    android:valueFrom="2"
+                    android:valueTo="22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.349,0 0.21,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="2667"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..3f3b288
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/default_dialog_corner_radius"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_close_button.xml b/quickstep/res/drawable/gesture_tutorial_close_button.xml
new file mode 100644
index 0000000..0702042
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_close_button.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="13dp"
+    android:viewportHeight="14"
+    android:viewportWidth="14"
+    android:width="13dp">
+    <path
+        android:fillColor="#000000"
+        android:fillType="evenOdd"
+        android:pathData="M14,1.41L12.59,0L7,5.59L1.41,0L0,1.41L5.59,7L0,12.59L1.41,14L7,8.41L12.59,14L14,12.59L8.41,7L14,1.41Z"/>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/home_gesture.xml b/quickstep/res/drawable/home_gesture.xml
new file mode 100644
index 0000000..4de29d0
--- /dev/null
+++ b/quickstep/res/drawable/home_gesture.xml
@@ -0,0 +1,935 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="412dp"
+            android:height="890dp"
+            android:viewportWidth="412"
+            android:viewportHeight="890">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_3_G_T_1"
+                    android:translateX="206"
+                    android:translateY="880.283">
+                    <group
+                        android:name="_R_G_L_3_G"
+                        android:translateX="-48.25"
+                        android:translateY="-51.643">
+                        <path
+                            android:name="_R_G_L_3_G_D_0_P_0"
+                            android:fillAlpha="0"
+                            android:fillColor="#e8f0fe"
+                            android:fillType="nonZero"
+                            android:pathData=" M96.25 48.25 C96.25,74.76 74.76,96.25 48.25,96.25 C21.74,96.25 0.25,74.76 0.25,48.25 C0.25,21.74 21.74,0.25 48.25,0.25 C74.76,0.25 96.25,21.74 96.25,48.25c " />
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_2_G_N_4_N_3_T_1"
+                    android:rotation="-60"
+                    android:translateX="513.995"
+                    android:translateY="909.041">
+                    <group
+                        android:name="_R_G_L_2_G_N_4_N_3_T_0"
+                        android:translateX="-125.282"
+                        android:translateY="-222.031">
+                        <group
+                            android:name="_R_G_L_2_G_N_4_T_0"
+                            android:translateX="-27.223000000000006"
+                            android:translateY="-19.78200000000001">
+                            <group
+                                android:name="_R_G_L_2_G"
+                                android:pivotX="49.356"
+                                android:pivotY="100.997"
+                                android:rotation="-4"
+                                android:translateX="-12.881"
+                                android:translateY="-68.965">
+                                <path
+                                    android:name="_R_G_L_2_G_D_0_P_0"
+                                    android:fillAlpha="1"
+                                    android:fillColor="#d2e3fc"
+                                    android:fillType="nonZero"
+                                    android:pathData=" M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 95.13,76.48 95.13,76.48 C98.47,94.41 92.03,116.16 52.12,115.41 C52.12,115.41 31.32,103.09 31.32,103.09 C23.59,101.22 5.39,82.22 3.78,59.83 C3.78,59.83 3.05,36.42 3.05,36.42 C5.71,17.29 15.16,6.55 29.51,3.44c " />
+                                <path
+                                    android:name="_R_G_L_2_G_D_1_P_0"
+                                    android:pathData=" M58.16 28.36 C59.49,32.32 59.42,36.53 52.4,38.99 C52.4,38.99 46.29,40.77 30.67,46.05 C28.57,46.76 25.79,48.06 23.78,47.5 C22.12,47.04 20.49,45.56 19.68,42.39 C16.89,31.56 15,24.2 18.96,19.11 "
+                                    android:strokeWidth="6"
+                                    android:strokeAlpha="1"
+                                    android:strokeColor="#a0c2f9" />
+                            </group>
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_1_G_N_3_T_1"
+                    android:rotation="-60"
+                    android:translateX="513.995"
+                    android:translateY="909.041">
+                    <group
+                        android:name="_R_G_L_1_G_N_3_T_0"
+                        android:translateX="-125.282"
+                        android:translateY="-222.031">
+                        <group
+                            android:name="_R_G_L_1_G"
+                            android:translateX="-27.223000000000006"
+                            android:translateY="-19.78200000000001">
+                            <path
+                                android:name="_R_G_L_1_G_D_0_P_0"
+                                android:fillAlpha="1"
+                                android:fillColor="#d2e3fc"
+                                android:fillType="nonZero"
+                                android:pathData=" M62.05 -4.24 C62.05,-4.24 76.43,1.82 76.43,1.82 C91.76,16.39 83.61,36.19 86.21,52.29 C92.02,88.24 84.86,126.82 117.41,193.95 C87.71,242.97 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 17.54,107.98 13.81,87.39 C10.28,67.94 10.58,45.16 10,31.95 C10.04,30.08 8.74,12.88 15.64,8.72 C26.15,2.4 40.27,-9.2 62.05,-4.24c " />
+                            <path
+                                android:name="_R_G_L_1_G_D_1_P_0"
+                                android:pathData=" M31.4 21.98 C31.4,21.98 41.87,1.7 68.18,3.97 "
+                                android:strokeWidth="6"
+                                android:strokeAlpha="1"
+                                android:strokeColor="#a0c2f9" />
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:rotation="-60"
+                    android:translateX="513.995"
+                    android:translateY="909.041">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="-125.282"
+                        android:translateY="-222.031">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#d2e3fc"
+                            android:fillType="nonZero"
+                            android:pathData=" M5.87 164.71 C0.43,267.95 56.57,389.47 139.31,416.77 C222.04,444.07 215.7,295.51 248.09,197.33 C280.49,99.14 111.3,154.92 55.02,40.63 C41.48,24.52 8.77,109.58 5.87,164.71c " />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_3_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="133"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="333"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="450"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="467"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="300"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="917"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="880.283"
+                    android:valueTo="880.283"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.247,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="883"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="880.283"
+                    android:valueTo="486.283"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.247,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 95.13,76.48 95.13,76.48 C98.47,94.41 92.03,116.16 52.12,115.41 C52.12,115.41 31.32,103.09 31.32,103.09 C23.59,101.22 5.39,82.22 3.78,59.83 C3.78,59.83 3.05,36.42 3.05,36.42 C5.71,17.29 15.16,6.55 29.51,3.44c "
+                    android:valueTo="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 95.13,76.48 95.13,76.48 C98.47,94.41 92.03,116.16 52.12,115.41 C52.12,115.41 31.32,103.09 31.32,103.09 C23.59,101.22 5.39,82.22 3.78,59.83 C3.78,59.83 3.05,36.42 3.05,36.42 C5.71,17.29 15.16,6.55 29.51,3.44c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 95.13,76.48 95.13,76.48 C98.47,94.41 92.03,116.16 52.12,115.41 C52.12,115.41 31.32,103.09 31.32,103.09 C23.59,101.22 5.39,82.22 3.78,59.83 C3.78,59.83 3.05,36.42 3.05,36.42 C5.71,17.29 15.16,6.55 29.51,3.44c "
+                    android:valueTo="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 81.12,76.53 81.12,76.53 C84.46,94.46 74.07,117.8 56.14,121.15 C56.14,121.15 51.31,122.05 51.31,122.05 C31.42,125.74 12.82,111.19 11.57,91 C11.57,91 6.61,34.54 6.61,34.54 C5.34,19.87 15.16,6.55 29.51,3.44c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="pathData"
+                    android:startOffset="917"
+                    android:valueFrom="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 81.12,76.53 81.12,76.53 C84.46,94.46 74.07,117.8 56.14,121.15 C56.14,121.15 51.31,122.05 51.31,122.05 C31.42,125.74 12.82,111.19 11.57,91 C11.57,91 6.61,34.54 6.61,34.54 C5.34,19.87 15.16,6.55 29.51,3.44c "
+                    android:valueTo="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 81.12,76.53 81.12,76.53 C84.46,94.46 74.07,117.8 56.14,121.15 C56.14,121.15 51.31,122.05 51.31,122.05 C31.42,125.74 12.82,111.19 11.57,91 C11.57,91 6.61,34.54 6.61,34.54 C5.34,19.87 15.16,6.55 29.51,3.44c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="pathData"
+                    android:startOffset="1483"
+                    android:valueFrom="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 81.12,76.53 81.12,76.53 C84.46,94.46 74.07,117.8 56.14,121.15 C56.14,121.15 51.31,122.05 51.31,122.05 C31.42,125.74 12.82,111.19 11.57,91 C11.57,91 6.61,34.54 6.61,34.54 C5.34,19.87 15.16,6.55 29.51,3.44c "
+                    android:valueTo="M29.51 3.44 C29.51,3.44 29.51,3.44 29.51,3.44 C44.17,0.25 58.86,8.71 63.48,22.99 C63.48,22.99 95.13,76.48 95.13,76.48 C98.47,94.41 92.03,116.16 52.12,115.41 C52.12,115.41 31.32,103.09 31.32,103.09 C23.59,101.22 5.39,82.22 3.78,59.83 C3.78,59.83 3.05,36.42 3.05,36.42 C5.71,17.29 15.16,6.55 29.51,3.44c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-4"
+                    android:valueTo="-4"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="200"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-4"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="2300"
+                    android:propertyName="rotation"
+                    android:startOffset="533"
+                    android:valueFrom="0"
+                    android:valueTo="-4"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="513.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="513.995"
+                    android:valueTo="469.109"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="translateX"
+                    android:startOffset="483"
+                    android:valueFrom="469.109"
+                    android:valueTo="343.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.094,0.355 0.269,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateX"
+                    android:startOffset="917"
+                    android:valueFrom="343.995"
+                    android:valueTo="367.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateX"
+                    android:startOffset="1483"
+                    android:valueFrom="367.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="909.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="909.041"
+                    android:valueTo="859.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateY"
+                    android:startOffset="917"
+                    android:valueFrom="859.041"
+                    android:valueTo="883.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateY"
+                    android:startOffset="1483"
+                    android:valueFrom="883.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-60"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-60"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="rotation"
+                    android:startOffset="917"
+                    android:valueFrom="0"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.545,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="rotation"
+                    android:startOffset="1483"
+                    android:valueFrom="-2"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M62.05 -4.24 C62.05,-4.24 76.43,1.82 76.43,1.82 C91.76,16.39 83.61,36.19 86.21,52.29 C92.02,88.24 84.86,126.82 117.41,193.95 C87.71,242.97 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 17.54,107.98 13.81,87.39 C10.28,67.94 10.58,45.16 10,31.95 C10.04,30.08 8.74,12.88 15.64,8.72 C26.15,2.4 40.27,-9.2 62.05,-4.24c "
+                    android:valueTo="M62.05 -4.24 C62.05,-4.24 76.43,1.82 76.43,1.82 C91.76,16.39 83.61,36.19 86.21,52.29 C92.02,88.24 84.86,126.82 117.41,193.95 C87.71,242.97 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 17.54,107.98 13.81,87.39 C10.28,67.94 10.58,45.16 10,31.95 C10.04,30.08 8.74,12.88 15.64,8.72 C26.15,2.4 40.27,-9.2 62.05,-4.24c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M62.05 -4.24 C62.05,-4.24 76.43,1.82 76.43,1.82 C91.76,16.39 83.61,36.19 86.21,52.29 C92.02,88.24 84.86,126.82 117.41,193.95 C87.71,242.97 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 17.54,107.98 13.81,87.39 C10.28,67.94 10.58,45.16 10,31.95 C10.04,30.08 8.74,12.88 15.64,8.72 C26.15,2.4 40.27,-9.2 62.05,-4.24c "
+                    android:valueTo="M51.44 5.02 C51.44,5.02 68.24,7.56 68.24,7.56 C73.26,21.67 72.7,36.77 79.13,51.76 C94.55,87.66 106.15,89.19 144.51,111.49 C114.82,160.51 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 19.02,105.73 15.29,85.14 C11.76,65.68 5.52,42.3 0.25,30.23 C0.29,28.36 0.58,17.81 15.64,8.72 C26.15,2.39 40.15,0.25 51.44,5.02c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="pathData"
+                    android:startOffset="917"
+                    android:valueFrom="M51.44 5.02 C51.44,5.02 68.24,7.56 68.24,7.56 C73.26,21.67 72.7,36.77 79.13,51.76 C94.55,87.66 106.15,89.19 144.51,111.49 C114.82,160.51 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 19.02,105.73 15.29,85.14 C11.76,65.68 5.52,42.3 0.25,30.23 C0.29,28.36 0.58,17.81 15.64,8.72 C26.15,2.39 40.15,0.25 51.44,5.02c "
+                    android:valueTo="M51.44 5.02 C51.44,5.02 68.24,7.56 68.24,7.56 C73.26,21.67 72.7,36.77 79.13,51.76 C94.55,87.66 106.15,89.19 144.51,111.49 C114.82,160.51 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 19.02,105.73 15.29,85.14 C11.76,65.68 5.52,42.3 0.25,30.23 C0.29,28.36 0.58,17.81 15.64,8.72 C26.15,2.39 40.15,0.25 51.44,5.02c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="pathData"
+                    android:startOffset="1483"
+                    android:valueFrom="M51.44 5.02 C51.44,5.02 68.24,7.56 68.24,7.56 C73.26,21.67 72.7,36.77 79.13,51.76 C94.55,87.66 106.15,89.19 144.51,111.49 C114.82,160.51 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 19.02,105.73 15.29,85.14 C11.76,65.68 5.52,42.3 0.25,30.23 C0.29,28.36 0.58,17.81 15.64,8.72 C26.15,2.39 40.15,0.25 51.44,5.02c "
+                    android:valueTo="M62.05 -4.24 C62.05,-4.24 76.43,1.82 76.43,1.82 C91.76,16.39 83.61,36.19 86.21,52.29 C92.02,88.24 84.86,126.82 117.41,193.95 C87.71,242.97 78.03,245.45 48.32,294.5 C48.32,294.5 18.2,137.31 18.2,137.31 C19.12,123.7 17.54,107.98 13.81,87.39 C10.28,67.94 10.58,45.16 10,31.95 C10.04,30.08 8.74,12.88 15.64,8.72 C26.15,2.4 40.27,-9.2 62.05,-4.24c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M31.4 21.98 C31.4,21.98 41.87,1.7 68.18,3.97 "
+                    android:valueTo="M31.4 21.98 C31.4,21.98 41.87,1.7 68.18,3.97 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M31.4 21.98 C31.4,21.98 41.87,1.7 68.18,3.97 "
+                    android:valueTo="M17.9 26.98 C17.9,26.98 29.87,13.7 56.18,15.97 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="pathData"
+                    android:startOffset="917"
+                    android:valueFrom="M17.9 26.98 C17.9,26.98 29.87,13.7 56.18,15.97 "
+                    android:valueTo="M17.9 26.98 C17.9,26.98 29.87,13.7 56.18,15.97 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="pathData"
+                    android:startOffset="1483"
+                    android:valueFrom="M17.9 26.98 C17.9,26.98 29.87,13.7 56.18,15.97 "
+                    android:valueTo="M31.4 21.98 C31.4,21.98 41.87,1.7 68.18,3.97 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="513.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="513.995"
+                    android:valueTo="469.109"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="translateX"
+                    android:startOffset="483"
+                    android:valueFrom="469.109"
+                    android:valueTo="343.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.094,0.355 0.269,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateX"
+                    android:startOffset="917"
+                    android:valueFrom="343.995"
+                    android:valueTo="367.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateX"
+                    android:startOffset="1483"
+                    android:valueFrom="367.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="909.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="909.041"
+                    android:valueTo="859.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateY"
+                    android:startOffset="917"
+                    android:valueFrom="859.041"
+                    android:valueTo="883.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateY"
+                    android:startOffset="1483"
+                    android:valueFrom="883.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-60"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-60"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="rotation"
+                    android:startOffset="917"
+                    android:valueFrom="0"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.545,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="rotation"
+                    android:startOffset="1483"
+                    android:valueFrom="-2"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M5.87 164.71 C0.43,267.95 56.57,389.47 139.31,416.77 C222.04,444.07 215.7,295.51 248.09,197.33 C280.49,99.14 111.3,154.92 55.02,40.63 C41.48,24.52 8.77,109.58 5.87,164.71c "
+                    android:valueTo="M5.87 164.71 C0.43,267.95 56.57,389.47 139.31,416.77 C222.04,444.07 215.7,295.51 248.09,197.33 C280.49,99.14 111.3,154.92 55.02,40.63 C41.48,24.52 8.77,109.58 5.87,164.71c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M5.87 164.71 C0.43,267.95 56.57,389.47 139.31,416.77 C222.04,444.07 215.7,295.51 248.09,197.33 C280.49,99.14 111.3,154.92 55.02,40.63 C41.48,24.52 8.77,109.58 5.87,164.71c "
+                    android:valueTo="M-9.23 115.79 C-30.45,216.96 -36.73,359.16 139.26,416.71 C222.07,443.78 220.51,278.25 244.3,175.03 C268.14,75.53 116.98,150.23 54.13,40.23 C40.58,24.12 5.18,47.09 -9.23,115.79c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="pathData"
+                    android:startOffset="483"
+                    android:valueFrom="M-9.23 115.79 C-30.45,216.96 -36.73,359.16 139.26,416.71 C222.07,443.78 220.51,278.25 244.3,175.03 C268.14,75.53 116.98,150.23 54.13,40.23 C40.58,24.12 5.18,47.09 -9.23,115.79c "
+                    android:valueTo="M-8.56 118.7 C-9.54,222.08 56.39,389.22 139.13,416.51 C221.86,443.81 234.66,227.56 233.16,109.56 C231.84,6.17 133.65,136.44 51.49,39.06 C37.94,22.95 -7.96,56.24 -8.56,118.7c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="917"
+                    android:propertyName="pathData"
+                    android:startOffset="917"
+                    android:valueFrom="M-8.56 118.7 C-9.54,222.08 56.39,389.22 139.13,416.51 C221.86,443.81 234.66,227.56 233.16,109.56 C231.84,6.17 133.65,136.44 51.49,39.06 C37.94,22.95 -7.96,56.24 -8.56,118.7c "
+                    android:valueTo="M-8.56 118.7 C-9.54,222.08 56.39,389.22 139.13,416.51 C221.86,443.81 238.66,196.56 237.16,78.56 C235.84,-24.83 133.65,136.44 51.49,39.06 C37.94,22.95 -7.96,56.24 -8.56,118.7c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.212,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1000"
+                    android:propertyName="pathData"
+                    android:startOffset="1833"
+                    android:valueFrom="M-8.56 118.7 C-9.54,222.08 56.39,389.22 139.13,416.51 C221.86,443.81 238.66,196.56 237.16,78.56 C235.84,-24.83 133.65,136.44 51.49,39.06 C37.94,22.95 -7.96,56.24 -8.56,118.7c "
+                    android:valueTo="M5.87 164.71 C0.43,267.95 56.57,389.47 139.31,416.77 C222.04,444.07 215.7,295.51 248.09,197.33 C280.49,99.14 111.3,154.92 55.02,40.63 C41.48,24.52 8.77,109.58 5.87,164.71c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="513.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="513.995"
+                    android:valueTo="469.109"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.711,0 0.772,0.166 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="translateX"
+                    android:startOffset="483"
+                    android:valueFrom="469.109"
+                    android:valueTo="343.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.094,0.355 0.269,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateX"
+                    android:startOffset="917"
+                    android:valueFrom="343.995"
+                    android:valueTo="367.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateX"
+                    android:startOffset="1483"
+                    android:valueFrom="367.995"
+                    android:valueTo="513.995"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="909.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="909.041"
+                    android:valueTo="859.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="translateY"
+                    android:startOffset="917"
+                    android:valueFrom="859.041"
+                    android:valueTo="883.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.475,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="translateY"
+                    android:startOffset="1483"
+                    android:valueFrom="883.041"
+                    android:valueTo="909.041"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-60"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-60"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="567"
+                    android:propertyName="rotation"
+                    android:startOffset="917"
+                    android:valueFrom="0"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.307,0 0.545,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1350"
+                    android:propertyName="rotation"
+                    android:startOffset="1483"
+                    android:valueFrom="-2"
+                    android:valueTo="-60"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.513,0 0.219,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="2850"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_screenshot.xml b/quickstep/res/drawable/ic_screenshot.xml
new file mode 100644
index 0000000..d97eae1
--- /dev/null
+++ b/quickstep/res/drawable/ic_screenshot.xml
@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,21L7,21v-1h10v1zM17,18L7,18L7,6h10v12zM17,4L7,4L7,3h10v1zM9.5,8.5L12,8.5L12,7L8,7v4h1.5zM12,17h4v-4h-1.5v2.5L12,15.5z"/>
+</vector>
diff --git a/quickstep/res/drawable/ic_share.xml b/quickstep/res/drawable/ic_share.xml
new file mode 100644
index 0000000..ff4baec
--- /dev/null
+++ b/quickstep/res/drawable/ic_share.xml
@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M18,16c-0.79,0 -1.5,0.31 -2.03,0.81L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.53,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.48 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.05,4.12c-0.05,0.22 -0.09,0.45 -0.09,0.69 0,1.66 1.34,3 3,3s3,-1.34 3,-3 -1.34,-3 -3,-3zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
+</vector>
diff --git a/quickstep/res/drawable/overview_gesture.xml b/quickstep/res/drawable/overview_gesture.xml
new file mode 100644
index 0000000..68c48d4
--- /dev/null
+++ b/quickstep/res/drawable/overview_gesture.xml
@@ -0,0 +1,1173 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="412dp"
+            android:height="890dp"
+            android:viewportWidth="412"
+            android:viewportHeight="890">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_3_G_N_4_N_3_N_2_T_0"
+                    android:translateX="297.398"
+                    android:translateY="721.169">
+                    <group
+                        android:name="_R_G_L_3_G_N_4_N_3_T_1"
+                        android:rotation="-45"
+                        android:translateX="110.176"
+                        android:translateY="177.218">
+                        <group
+                            android:name="_R_G_L_3_G_N_4_N_3_T_0"
+                            android:translateX="-132.239"
+                            android:translateY="-133.055">
+                            <group
+                                android:name="_R_G_L_3_G_N_4_T_0"
+                                android:pivotX="71.634"
+                                android:pivotY="92.684"
+                                android:rotation="-2"
+                                android:translateX="-36.948"
+                                android:translateY="-58.704">
+                                <group
+                                    android:name="_R_G_L_3_G"
+                                    android:pivotX="48.25"
+                                    android:pivotY="48.25"
+                                    android:rotation="-47"
+                                    android:translateX="-20.55"
+                                    android:translateY="-21.306">
+                                    <path
+                                        android:name="_R_G_L_3_G_D_0_P_0"
+                                        android:fillAlpha="0"
+                                        android:fillColor="#e8f0fe"
+                                        android:fillType="nonZero"
+                                        android:pathData=" M96.25 48.25 C96.25,74.76 74.76,96.25 48.25,96.25 C21.74,96.25 0.25,74.76 0.25,48.25 C0.25,21.74 21.74,0.25 48.25,0.25 C74.76,0.25 96.25,21.74 96.25,48.25c " />
+                                </group>
+                            </group>
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_2_G"
+                    android:pivotX="48.25"
+                    android:pivotY="48.25"
+                    android:rotation="-47"
+                    android:translateX="152.837"
+                    android:translateY="698.322">
+                    <path
+                        android:name="_R_G_L_2_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#e8f0fe"
+                        android:fillType="nonZero"
+                        android:pathData=" M96.25 48.25 C96.25,74.76 74.76,96.25 48.25,96.25 C21.74,96.25 0.25,74.76 0.25,48.25 C0.25,21.74 21.74,0.25 48.25,0.25 C74.76,0.25 96.25,21.74 96.25,48.25c " />
+                </group>
+                <group
+                    android:name="_R_G_L_1_G_N_3_N_2_T_0"
+                    android:translateX="297.398"
+                    android:translateY="721.169">
+                    <group
+                        android:name="_R_G_L_1_G_N_3_T_1"
+                        android:rotation="-45"
+                        android:translateX="110.176"
+                        android:translateY="177.218">
+                        <group
+                            android:name="_R_G_L_1_G_N_3_T_0"
+                            android:translateX="-132.239"
+                            android:translateY="-133.055">
+                            <group
+                                android:name="_R_G_L_1_G"
+                                android:pivotX="71.634"
+                                android:pivotY="92.684"
+                                android:rotation="-2"
+                                android:translateX="-36.948"
+                                android:translateY="-58.704">
+                                <path
+                                    android:name="_R_G_L_1_G_D_0_P_0"
+                                    android:fillAlpha="1"
+                                    android:fillColor="#d2e3fc"
+                                    android:fillType="nonZero"
+                                    android:pathData=" M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 96.98,58.55 96.98,58.55 C113.17,83.98 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c " />
+                                <path
+                                    android:name="_R_G_L_1_G_D_1_P_0"
+                                    android:pathData=" M50.61 24.81 C53.83,30.61 58.53,37.85 51.26,44.87 C51.26,44.87 38.64,50.25 34.66,52.09 C32.65,53.03 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                                    android:strokeWidth="6"
+                                    android:strokeAlpha="1"
+                                    android:strokeColor="#a0c2f9"/>
+                            </group>
+                        </group>
+                    </group>
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_N_2_T_0"
+                    android:translateX="297.398"
+                    android:translateY="721.169">
+                    <group
+                        android:name="_R_G_L_0_G_T_1"
+                        android:rotation="-45"
+                        android:translateX="110.176"
+                        android:translateY="177.218">
+                        <group
+                            android:name="_R_G_L_0_G"
+                            android:translateX="-132.239"
+                            android:translateY="-133.055">
+                            <path
+                                android:name="_R_G_L_0_G_D_0_P_0"
+                                android:fillAlpha="1"
+                                android:fillColor="#d2e3fc"
+                                android:fillType="nonZero"
+                                android:pathData=" M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 136.7,91.72 185.58,117.71 C221.01,256.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c " />
+                            <path
+                                android:name="_R_G_L_0_G_D_1_P_0"
+                                android:pathData=" M26.74 49.32 C26.74,49.32 34.17,35.86 51.22,27.28 "
+                                android:strokeWidth="6"
+                                android:strokeAlpha="1"
+                                android:strokeColor="#a0c2f9"/>
+                        </group>
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_3_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="217"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="333"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_N_4_T_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-2"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-2"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1100"
+                    android:propertyName="rotation"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="rotation"
+                    android:startOffset="2267"
+                    android:valueFrom="0"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="110.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="110.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="933"
+                    android:propertyName="translateX"
+                    android:startOffset="1167"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateX"
+                    android:startOffset="2100"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateX"
+                    android:startOffset="2267"
+                    android:valueFrom="45.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="177.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="177.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1167"
+                    android:valueFrom="190.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="1767"
+                    android:valueFrom="190.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateY"
+                    android:startOffset="2100"
+                    android:valueFrom="201.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateY"
+                    android:startOffset="2267"
+                    android:valueFrom="201.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_N_4_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-45"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-45"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="1767"
+                    android:valueFrom="0"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="rotation"
+                    android:startOffset="2100"
+                    android:valueFrom="-1"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="rotation"
+                    android:startOffset="2267"
+                    android:valueFrom="-1"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_3_G_N_4_N_3_N_2_T_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleY"
+                    android:startOffset="1167"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1767"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="1767"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleX"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 96.98,58.55 96.98,58.55 C113.17,83.98 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueTo="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 96.98,58.55 96.98,58.55 C113.17,83.98 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 96.98,58.55 96.98,58.55 C113.17,83.98 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueTo="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 94.24,55.98 94.24,55.98 C109.11,75.87 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1100"
+                    android:propertyName="pathData"
+                    android:startOffset="1167"
+                    android:valueFrom="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 94.24,55.98 94.24,55.98 C109.11,75.87 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueTo="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 94.24,55.98 94.24,55.98 C109.11,75.87 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="pathData"
+                    android:startOffset="2267"
+                    android:valueFrom="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 94.24,55.98 94.24,55.98 C109.11,75.87 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueTo="M20.63 8.95 C20.63,8.95 20.63,8.95 20.63,8.95 C32.8,0.25 49.72,2.9 59.23,14.69 C59.23,14.69 96.98,58.55 96.98,58.55 C113.17,83.98 104.58,100.89 89.25,111.3 C89.25,111.3 85.05,113.83 85.05,113.83 C67.95,125.12 45.66,118.31 36.77,99.32 C36.77,99.32 11.14,47.2 11.14,47.2 C4.45,34.1 8.44,17.28 20.63,8.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M50.61 24.81 C53.83,30.61 58.53,37.85 51.26,44.87 C51.26,44.87 38.64,50.25 34.66,52.09 C32.65,53.03 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueTo="M50.61 24.81 C53.83,30.61 58.53,37.85 51.26,44.87 C51.26,44.87 38.64,50.25 34.66,52.09 C32.65,53.03 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M50.61 24.81 C53.83,30.61 58.53,37.85 51.26,44.87 C51.26,44.87 38.64,50.25 34.66,52.09 C32.65,53.03 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueTo="M57.62 26.47 C60,29.91 61.1,33.98 55.03,38.28 C55.03,38.28 49.65,41.69 36.11,51.08 C34.29,52.35 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1100"
+                    android:propertyName="pathData"
+                    android:startOffset="1167"
+                    android:valueFrom="M57.62 26.47 C60,29.91 61.1,33.98 55.03,38.28 C55.03,38.28 49.65,41.69 36.11,51.08 C34.29,52.35 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueTo="M57.62 26.47 C60,29.91 61.1,33.98 55.03,38.28 C55.03,38.28 49.65,41.69 36.11,51.08 C34.29,52.35 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="pathData"
+                    android:startOffset="2267"
+                    android:valueFrom="M57.62 26.47 C60,29.91 61.1,33.98 55.03,38.28 C55.03,38.28 49.65,41.69 36.11,51.08 C34.29,52.35 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueTo="M50.61 24.81 C53.83,30.61 58.53,37.85 51.26,44.87 C51.26,44.87 38.64,50.25 34.66,52.09 C32.65,53.03 31.97,54.36 29.89,54.38 C28.17,54.4 26.19,53.43 24.53,50.61 C18.85,40.97 15,34.43 17.4,28.44 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-2"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-2"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1100"
+                    android:propertyName="rotation"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="rotation"
+                    android:startOffset="2267"
+                    android:valueFrom="0"
+                    android:valueTo="-2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="110.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="110.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="933"
+                    android:propertyName="translateX"
+                    android:startOffset="1167"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateX"
+                    android:startOffset="2100"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateX"
+                    android:startOffset="2267"
+                    android:valueFrom="45.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="177.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="177.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1167"
+                    android:valueFrom="190.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="1767"
+                    android:valueFrom="190.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateY"
+                    android:startOffset="2100"
+                    android:valueFrom="201.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateY"
+                    android:startOffset="2267"
+                    android:valueFrom="201.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_N_3_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-45"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-45"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="1767"
+                    android:valueFrom="0"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="rotation"
+                    android:startOffset="2100"
+                    android:valueFrom="-1"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="rotation"
+                    android:startOffset="2267"
+                    android:valueFrom="-1"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 136.7,91.72 185.58,117.71 C221.01,256.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueTo="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 136.7,91.72 185.58,117.71 C221.01,256.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.425,0 0.463,0.469 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="700"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 136.7,91.72 185.58,117.71 C221.01,256.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueTo="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.425,0 0.463,0.469 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="733"
+                    android:propertyName="pathData"
+                    android:startOffset="1033"
+                    android:valueFrom="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueTo="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.275,0.272 0.661,0.665 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="pathData"
+                    android:startOffset="1767"
+                    android:valueFrom="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueTo="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.165 0.661,0.665 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="733"
+                    android:propertyName="pathData"
+                    android:startOffset="2267"
+                    android:valueFrom="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 148.67,90.67 218.58,75.71 C254.01,214.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueTo="M42.21 4.34 C42.21,4.34 58.68,0.25 58.68,0.25 C68.53,11.64 74.12,25.6 85.21,38.39 C115.84,72.27 136.7,91.72 185.58,117.71 C221.01,256.74 312.66,319.67 166.82,292.86 C166.82,292.86 61.19,143.45 61.19,143.45 C57.04,130.17 50.08,112.95 39.06,94.86 C28.46,77.49 14.05,57.75 4.69,48.2 C4.2,46.35 0.25,36.08 10.96,21.83 C17.93,11.59 30.17,4.01 42.21,4.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0.445 0.528,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M26.74 49.32 C26.74,49.32 34.17,35.86 51.22,27.28 "
+                    android:valueTo="M26.74 49.32 C26.74,49.32 34.17,35.86 51.22,27.28 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.55,0 0.375,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="pathData"
+                    android:startOffset="333"
+                    android:valueFrom="M26.74 49.32 C26.74,49.32 34.17,35.86 51.22,27.28 "
+                    android:valueTo="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.55,0 0.375,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="pathData"
+                    android:startOffset="1167"
+                    android:valueFrom="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueTo="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="1767"
+                    android:valueFrom="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueTo="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="pathData"
+                    android:startOffset="2100"
+                    android:valueFrom="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueTo="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="pathData"
+                    android:startOffset="2267"
+                    android:valueFrom="M32.48 57.18 C32.48,57.18 40.18,30.6 69.87,29.66 "
+                    android:valueTo="M26.74 49.32 C26.74,49.32 34.17,35.86 51.22,27.28 "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="110.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateX"
+                    android:startOffset="333"
+                    android:valueFrom="110.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="933"
+                    android:propertyName="translateX"
+                    android:startOffset="1167"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateX"
+                    android:startOffset="2100"
+                    android:valueFrom="45.176"
+                    android:valueTo="45.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateX"
+                    android:startOffset="2267"
+                    android:valueFrom="45.176"
+                    android:valueTo="110.176"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="177.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="translateY"
+                    android:startOffset="333"
+                    android:valueFrom="177.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.658,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="translateY"
+                    android:startOffset="1167"
+                    android:valueFrom="190.218"
+                    android:valueTo="190.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.401,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateY"
+                    android:startOffset="1767"
+                    android:valueFrom="190.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="translateY"
+                    android:startOffset="2100"
+                    android:valueFrom="201.218"
+                    android:valueTo="201.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="translateY"
+                    android:startOffset="2267"
+                    android:valueFrom="201.218"
+                    android:valueTo="177.218"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-45"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="833"
+                    android:propertyName="rotation"
+                    android:startOffset="333"
+                    android:valueFrom="-45"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.618,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="600"
+                    android:propertyName="rotation"
+                    android:startOffset="1167"
+                    android:valueFrom="0"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.348,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="rotation"
+                    android:startOffset="1767"
+                    android:valueFrom="0"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="rotation"
+                    android:startOffset="2100"
+                    android:valueFrom="-1"
+                    android:valueTo="-1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.539,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1233"
+                    android:propertyName="rotation"
+                    android:startOffset="2267"
+                    android:valueFrom="-1"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.286,0 0.489,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="3500"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml
new file mode 100644
index 0000000..4dc8913
--- /dev/null
+++ b/quickstep/res/layout/gesture_tutorial_activity.xml
@@ -0,0 +1,19 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gesture_tutorial_fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
new file mode 100644
index 0000000..459d65f
--- /dev/null
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -0,0 +1,121 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?android:attr/colorBackground">
+
+    <View
+        android:id="@+id/gesture_tutorial_ripple_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/gesture_tutorial_ripple"/>
+
+    <View
+        android:id="@+id/gesture_tutorial_fake_task_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/gesture_tutorial_fake_task_view_color"
+        android:visibility="invisible" />
+
+    <ImageView
+        android:id="@+id/gesture_tutorial_fragment_hand_coaching"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"/>
+
+    <ImageButton
+        android:id="@+id/gesture_tutorial_fragment_close_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="18dp"
+        android:layout_marginTop="30dp"
+        android:layout_marginStart="4dp"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"
+        android:background="@android:color/transparent"
+        android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
+        android:contentDescription="@string/gesture_tutorial_close_button_content_description"
+        android:tint="?android:attr/textColorPrimary"
+        android:src="@drawable/gesture_tutorial_close_button"/>
+
+    <LinearLayout
+        android:id="@+id/gesture_tutorial_fragment_titles_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="70dp"
+        android:layout_alignParentTop="true"
+        android:focusable="true"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/gesture_tutorial_fragment_title_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
+            android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
+            style="@style/TextAppearance.GestureTutorial.Title"/>
+
+        <TextView
+            android:id="@+id/gesture_tutorial_fragment_subtitle_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
+            android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
+            style="@style/TextAppearance.GestureTutorial.Subtitle"/>
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/gesture_tutorial_fragment_feedback_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="10dp"
+        android:layout_centerHorizontal="true"
+        android:layout_above="@id/gesture_tutorial_fragment_action_button"
+        android:layout_marginStart="@dimen/gesture_tutorial_feedback_margin_start_end"
+        android:layout_marginEnd="@dimen/gesture_tutorial_feedback_margin_start_end"
+        style="@style/TextAppearance.GestureTutorial.Feedback"/>
+
+    <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
+         of elevation and shadow) which is replaced by ripple effect in android:foreground -->
+    <Button
+        android:id="@+id/gesture_tutorial_fragment_action_button"
+        android:layout_width="142dp"
+        android:layout_height="49dp"
+        android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
+        android:layout_alignParentEnd="true"
+        android:layout_alignParentBottom="true"
+        android:stateListAnimator="@null"
+        android:background="@drawable/gesture_tutorial_action_button_background"
+        android:foreground="?android:attr/selectableItemBackgroundBorderless"
+        style="@style/TextAppearance.GestureTutorial.ButtonLabel"/>
+
+    <Button
+        android:id="@+id/gesture_tutorial_fragment_action_text_button"
+        android:layout_width="142dp"
+        android:layout_height="49dp"
+        android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentBottom="true"
+        android:stateListAnimator="@null"
+        android:background="@null"
+        android:foreground="?android:attr/selectableItemBackgroundBorderless"
+        style="@style/TextAppearance.GestureTutorial.TextButtonLabel"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
new file mode 100644
index 0000000..e05688e
--- /dev/null
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -0,0 +1,68 @@
+<?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.quickstep.views.OverviewActionsView 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"
+    android:layout_marginLeft="@dimen/overview_actions_horizontal_margin"
+    android:layout_marginRight="@dimen/overview_actions_horizontal_margin">
+
+    <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_screenshot"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableStart="@drawable/ic_screenshot"
+            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/share_space"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1"
+            android:visibility="gone" />
+    </LinearLayout>
+
+</com.android.quickstep.views.OverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 60cfa0c..f9bb2f2 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -31,7 +31,6 @@
         android:id="@+id/icon"
         android:layout_width="@dimen/task_thumbnail_icon_size"
         android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:layout_gravity="top|center_horizontal"
         android:focusable="false"
         android:importantForAccessibility="no"/>
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
deleted file mode 100644
index 2d1418e..0000000
--- a/quickstep/res/values-en-rCA/strings.xml
+++ /dev/null
@@ -1,37 +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.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="recent_task_option_split_screen" msgid="5353188922202653570">"Split screen"</string>
-    <string name="recent_task_option_pin" msgid="7929860679018978258">"Pin"</string>
-    <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
-    <string name="accessibility_desc_recent_apps" msgid="1444379410873162882">"Overview"</string>
-    <string name="recents_empty_message" msgid="7040467240571714191">"No recent items"</string>
-    <string name="accessibility_close_task" msgid="5354563209433803643">"Close"</string>
-    <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"App usage settings"</string>
-    <string name="recents_clear_all" msgid="5328176793634888831">"Clear all"</string>
-    <string name="accessibility_recent_apps" msgid="4058661986695117371">"Recent apps"</string>
-    <string name="task_contents_description_with_remaining_time" msgid="4479688746574672685">"<xliff:g id="TASK_DESCRIPTION">%1$s</xliff:g>, <xliff:g id="REMAINING_TIME">%2$s</xliff:g>"</string>
-    <string name="shorter_duration_less_than_one_minute" msgid="4722015666335015336">"&lt; 1 minute"</string>
-    <string name="time_left_for_app" msgid="3111996412933644358">"<xliff:g id="TIME">%1$s</xliff:g> left today"</string>
-    <string name="title_app_suggestions" msgid="4185902664111965088">"App suggestions"</string>
-    <string name="all_apps_label" msgid="8542784161730910663">"All apps"</string>
-    <string name="all_apps_prediction_tip" msgid="2672336544844936186">"Your predicted apps"</string>
-</resources>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
deleted file mode 100644
index bb186db..0000000
--- a/quickstep/res/values-en-rXC/strings.xml
+++ /dev/null
@@ -1,37 +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.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="recent_task_option_split_screen" msgid="5353188922202653570">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‎‎‎‏‎‎Split screen‎‏‎‎‏‎"</string>
-    <string name="recent_task_option_pin" msgid="7929860679018978258">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‏‎‎‏‎‎Pin‎‏‎‎‏‎"</string>
-    <string name="recent_task_option_freeform" msgid="48863056265284071">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‏‏‎‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‎‎‏‏‎‏‎‏‏‏‎‏‎‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‎‎‏‏‏‎Freeform‎‏‎‎‏‎"</string>
-    <string name="accessibility_desc_recent_apps" msgid="1444379410873162882">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‎‏‏‎‏‏‏‎‏‏‏‎‏‏‎‏‎‏‏‏‏‏‎‏‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‎‎‏‎‎‎‎‎‏‎‎Overview‎‏‎‎‏‎"</string>
-    <string name="recents_empty_message" msgid="7040467240571714191">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‏‎‏‎‎‏‏‏‎‎‏‏‏‏‏‏‎‎‎‎‏‏‎‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‏‎No recent items‎‏‎‎‏‎"</string>
-    <string name="accessibility_close_task" msgid="5354563209433803643">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‏‏‏‎‎‏‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‎‎‏‎‎‏‏‎‏‎‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‏‎Close‎‏‎‎‏‎"</string>
-    <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‎‏‏‎‏‎‎‏‏‎‏‏‎‎‎‎‏‎‏‎‏‎‎‎‏‏‏‎‏‎‏‎‏‎App usage settings‎‏‎‎‏‎"</string>
-    <string name="recents_clear_all" msgid="5328176793634888831">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‏‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‏‏‏‎‎‎‏‏‏‏‏‏‏‎Clear all‎‏‎‎‏‎"</string>
-    <string name="accessibility_recent_apps" msgid="4058661986695117371">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‏‎‎‏‏‎‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‏‎‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‎‏‏‎Recent apps‎‏‎‎‏‎"</string>
-    <string name="task_contents_description_with_remaining_time" msgid="4479688746574672685">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‏‏‎‎‎‎‏‏‎‏‎‎‏‎‏‏‏‎‎‎‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‎‏‏‎‎‏‎‏‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="TASK_DESCRIPTION">%1$s</xliff:g>‎‏‎‎‏‏‏‎, ‎‏‎‎‏‏‎<xliff:g id="REMAINING_TIME">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="shorter_duration_less_than_one_minute" msgid="4722015666335015336">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‎‏‎‏‏‏‎‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‎‎‎‎‏‏‎‏‎‏‎‎‎‎&lt; 1 minute‎‏‎‎‏‎"</string>
-    <string name="time_left_for_app" msgid="3111996412933644358">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="TIME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ left today‎‏‎‎‏‎"</string>
-    <string name="title_app_suggestions" msgid="4185902664111965088">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‎‏‏‏‎‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‏‎‎‎‏‏‏‎‏‏‎‎‎‎‎‏‎‏‎‎‎‎‏‏‏‎‏‎‎‎‎‎‎App suggestions‎‏‎‎‏‎"</string>
-    <string name="all_apps_label" msgid="8542784161730910663">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‏‏‎‎‎‎‏‎‎‎‎‏‏‏‎‏‏‏‎‏‏‏‏‏‎‎‎‏‏‎‎‏‏‎‏‏‏‎‏‏‎‎‏‏‏‎‎‎‏‏‏‎All apps‎‏‎‎‏‎"</string>
-    <string name="all_apps_prediction_tip" msgid="2672336544844936186">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‎‎‏‎‏‏‏‏‏‎‏‏‎‏‏‏‎‎‏‎‏‏‏‏‎‎‎‏‏‎‏‏‏‏‎‎‏‏‏‏‏‏‏‎‏‎‎Your predicted apps‎‏‎‎‏‎"</string>
-</resources>
diff --git a/go/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
similarity index 71%
rename from go/quickstep/res/values/colors.xml
rename to quickstep/res/values/colors.xml
index ff9dc9c..40da136 100644
--- a/go/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      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,
@@ -15,6 +14,6 @@
      limitations under the License.
 -->
 <resources>
-    <color name="clear_all_button_bg">#FFDADCE0</color>
-    <color name="clear_all_button_text">#FF5F6368</color>
-</resources>
+    <color name="back_arrow_color_light">#FFFFFFFF</color>
+    <color name="back_arrow_color_dark">#99000000</color>
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 292eaaa..0f2955b 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -14,12 +14,12 @@
      limitations under the License.
 -->
 <resources>
-    <string name="task_overlay_factory_class" translatable="false"></string>
+    <string name="task_overlay_factory_class" translatable="false"/>
 
-    <string name="overview_callbacks_class" translatable="false"></string>
-
-    <!-- Activity which blocks home gesture -->
-    <string name="gesture_blocking_activity" translatable="false"></string>
+    <!-- Activities which block home gesture -->
+    <string-array name="gesture_blocking_activities" translatable="false">
+        <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
+    </string-array>
 
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
 
@@ -33,4 +33,6 @@
     <!-- Assistant Gesture -->
     <integer name="assistant_gesture_min_time_threshold">200</integer>
     <integer name="assistant_gesture_corner_deg_threshold">20</integer>
+
+    <string name="wellbeing_provider_pkg" translatable="false"/>
 </resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 78424ca..9d70316 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -22,6 +22,12 @@
     <!-- For screens without rounded corners -->
     <dimen name="task_corner_radius_small">2dp</dimen>
 
+    <!-- Overrideable in overlay that provides the Overview Actions. -->
+    <dimen name="overview_actions_height">66dp</dimen>
+    <dimen name="overview_actions_bottom_margin_gesture">16dp</dimen>
+    <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_clear_all_deadzone_vertical_margin">70dp</dimen>
     <dimen name="overview_peek_distance">96dp</dimen>
@@ -56,15 +62,14 @@
     <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">136dp</dimen>
+    <dimen name="portrait_task_card_horz_space_big_overview">96dp</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>
     <!-- Copied from framework resource:
        docked_stack_divider_thickness - 2 * docked_stack_divider_insets -->
     <dimen name="multi_window_task_divider_size">10dp</dimen>
 
-    <!-- same as vertical_drag_handle_size -->
-    <dimen name="shelf_surface_offset">24dp</dimen>
-
     <!-- Assistant Gestures -->
     <!-- Distance from the vertical edges of the screen in which assist gestures are recognized -->
     <dimen name="gestures_assistant_width">48dp</dimen>
@@ -73,4 +78,24 @@
 
     <!-- Distance to move elements when swiping up to go home from launcher -->
     <dimen name="home_pullback_distance">28dp</dimen>
+
+    <!-- Distance to move the tasks when swiping up while the device is locked -->
+    <dimen name="device_locked_y_offset">-80dp</dimen>
+
+    <!-- Overscroll Gesture -->
+    <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
+    <dimen name="gestures_overscroll_active_threshold">80dp</dimen>
+    <dimen name="gestures_overscroll_finish_threshold">136dp</dimen>
+
+    <!-- Tips Gesture Tutorial -->
+    <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
+    <dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+    <dimen name="gesture_tutorial_feedback_margin_start_end">24dp</dimen>
+    <dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
+
+    <!-- All Apps Education tutorial -->
+    <dimen name="swipe_edu_padding">8dp</dimen>
+    <dimen name="swipe_edu_circle_size">64dp</dimen>
+    <dimen name="swipe_edu_width">80dp</dimen>
+    <dimen name="swipe_edu_max_height">184dp</dimen>
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 4319b5d..86120e3 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -29,15 +29,9 @@
     <!-- Title for an option to enter freeform mode for a given app -->
     <string name="recent_task_option_freeform">Freeform</string>
 
-    <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_desc_recent_apps">Overview</string>
-
     <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
     <string name="recents_empty_message">No recent items</string>
 
-    <!-- Content description for the recent apps's accessibility option that closes it. [CHAR LIMIT=NONE] -->
-    <string name="accessibility_close_task">Close</string>
-
     <!-- Content description for the recent apps's accessibility option that opens its usage settings. [CHAR LIMIT=NONE] -->
     <string name="accessibility_app_usage_settings">App usage settings</string>
 
@@ -66,5 +60,107 @@
     <!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
     <string name="all_apps_prediction_tip">Your predicted apps</string>
 
+    <!-- Content description for a close button. [CHAR LIMIT=NONE] -->
+    <string  name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
-</resources>
\ No newline at end of file
+    <!-- Hotseat educational strings for users who don't qualify for migration -->
+    <string name="hotseat_edu_title_migrate">Get app suggestions on the bottom row of your Home screen</string>
+    <string name="hotseat_edu_title_migrate_landscape">Get app suggestions on favorites row of your Home screen</string>
+
+    <string name="hotseat_edu_message_migrate">Easily access your most-used apps right on the Home screen. Suggestions will change based on your routines. Apps on the bottom row will move up to your Home screen. </string>
+    <string name="hotseat_edu_message_migrate_landscape">Easily access your most-used apps right on the Home screen. Suggestions will change based on your routines. Apps in favorites row will move to your Home screen. </string>
+    <string name="hotseat_edu_message_migrate_alt">Easily access your most-used apps, right on the Home screen. Suggestions will change based on your routines. Apps on the bottom row will move to a new folder.</string>
+
+    <!-- Button text to opt in for fully predicted hotseat -->
+    <string name="hotseat_edu_accept">Get app suggestions</string>
+    <!-- Button text to dismiss opt in for fully predicted hotseat -->
+    <string name="hotseat_edu_dismiss">No thanks</string>
+
+    <!-- action shown to toggle predictions after onboarding -->
+    <string name="hotseat_prediction_settings">Settings</string>
+
+    <!-- tip shown if user has no items in hotseat to migrate -->
+    <string name="hotseat_auto_enrolled">Most-used apps appear here, and change based on routines</string>
+    <!-- tip shown if user declines migration and has no open spots for prediction -->
+    <string name="hotseat_tip_no_empty_slots">Drag apps off the bottom row to get app suggestions</string>
+    <!-- tip shown if user declines migration and has some open spots for prediction -->
+    <string name="hotseat_tip_gaps_filled">App suggestions added to empty space</string>
+    <!-- tip shown when user migrates and predictions are enabled in hotseat -->
+    <string name="hotsaet_tip_prediction_enabled">App suggestions enabled</string>
+    <!-- tip shown when hotseat edu is requested while predictions are disabled -->
+    <string name="hotsaet_tip_prediction_disabled">App suggestions are disabled</string>
+
+    <!-- content description for hotseat items -->
+    <string name="hotseat_prediction_content_description">Predicted app: <xliff:g id="title" example="Chrome">%1$s</xliff:g></string>
+
+    <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
+    <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
+    <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge" translatable="false">Start at the right edge and swipe toward the middle</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_too_far_from_right_edge" translatable="false">Make sure you swipe from the far right edge</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is cancelled. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_cancelled_right_edge" translatable="false">Make sure you swipe straight to the left and let go</string>
+
+    <!-- Title shown during interactive part of Back gesture tutorial for left edge. [CHAR LIMIT=30] -->
+    <string name="back_gesture_tutorial_playground_title_swipe_inward_left_edge" translatable="false">Try the other side</string>
+    <!-- Subtitle shown during interactive parts of Back gesture tutorial for left edge. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge" translatable="false">That\'s it! Now try swiping from the left edge.</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_too_far_from_left_edge" translatable="false">Make sure you swipe from the far left edge</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is cancelled. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_cancelled_left_edge" translatable="false">Make sure you swipe straight to the right and let go</string>
+
+    <!-- Feedback shown during interactive parts of Back gesture tutorial when the gesture is within the nav bar region. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_in_nav_bar" translatable="false">Make sure you don\'t swipe too close to the bottom of the screen</string>
+    <!-- Subtitle shown on the confirmation screen after successful gesture. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
+
+    <!-- Title shown during interactive part of Home gesture tutorial. [CHAR LIMIT=30] -->
+    <string name="home_gesture_tutorial_playground_title" translatable="false">Tutorial: Go Home</string>
+    <!-- Subtitle shown during interactive parts of Home gesture tutorial. [CHAR LIMIT=60] -->
+    <string name="home_gesture_tutorial_playground_subtitle" translatable="false">Try swiping upward from the bottom edge of the screen</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is started too far from the edge. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the bottom edge of the screen</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the Overview gesture is detected. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_overview_detected" translatable="false">Make sure you don\'t pause before letting go</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up</string>
+
+    <!-- Title shown during interactive part of Overview gesture tutorial. [CHAR LIMIT=30] -->
+    <string name="overview_gesture_tutorial_playground_title" translatable="false">Tutorial: Switch Apps</string>
+    <!-- Subtitle shown during interactive parts of Overview gesture tutorial. [CHAR LIMIT=60] -->
+    <string name="overview_gesture_tutorial_playground_subtitle" translatable="false">Swipe up from the bottom of the screen and hold</string>
+    <!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is started too far from the edge. [CHAR LIMIT=100] -->
+    <string name="overview_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the bottom edge of the screen</string>
+    <!-- Feedback shown during interactive parts of Overview gesture tutorial when the Home gesture is detected. [CHAR LIMIT=100] -->
+    <string name="overview_gesture_feedback_home_detected" translatable="false">Try holding the window for longer before releasing</string>
+    <!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
+    <string name="overview_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up and pause</string>
+
+    <!-- Title shown during interactive part of Assistant gesture tutorial. [CHAR LIMIT=30] -->
+    <string name="assistant_gesture_tutorial_playground_title" translatable="false">Tutorial: Assistant</string>
+    <!-- Subtitle shown during interactive parts of Assistant gesture tutorial. [CHAR LIMIT=60] -->
+    <string name="assistant_gesture_tutorial_playground_subtitle" translatable="false">Try swiping diagonally from a bottom corner of the screen</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture is started too far from the corner. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go diagonally enough. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go far enough. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further</string>
+
+    <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
+    <string name="gesture_tutorial_confirm_title" translatable="false">All set</string>
+    <!-- Button text shown on a button on the confirm screen to leave the tutorial. [CHAR LIMIT=14] -->
+    <string name="gesture_tutorial_action_button_label_done" translatable="false">Done</string>
+    <!-- Button text shown on a button to go to Settings. [CHAR LIMIT=14] -->
+    <string name="gesture_tutorial_action_button_label_settings" translatable="false">Settings</string>
+
+    <!-- ******* Overview ******* -->
+    <!-- Label for a button that causes the current overview app to be shared. [CHAR_LIMIT=40] -->
+    <string name="action_share">Share</string>
+    <!-- Label for a button that causes a screen shot of the current app to be taken. [CHAR_LIMIT=40] -->
+    <string name="action_screenshot">Screenshot</string>
+    <!-- Message shown when an action is blocked by a policy enforced by the app or the organization managing the device. [CHAR_LIMIT=NONE] -->
+    <string name="blocked_by_policy">This action isn\'t allowed by the app or your organization</string>
+</resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index bb364ff..8d054b4 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -25,4 +25,64 @@
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
     </style>
+
+    <style name="TextAppearance.GestureTutorial"
+        parent="android:TextAppearance.Material.Body1" />
+
+    <style name="TextAppearance.GestureTutorial.CallToAction"
+        parent="android:TextAppearance.Material.Body2" />
+
+    <style name="TextAppearance.GestureTutorial.Title"
+        parent="TextAppearance.GestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">28sp</item>
+    </style>
+
+    <style name="TextAppearance.GestureTutorial.Subtitle"
+        parent="TextAppearance.GestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">?android:attr/textColorTertiary</item>
+        <item name="android:letterSpacing">0.03</item>
+        <item name="android:textSize">21sp</item>
+    </style>
+
+    <style name="TextAppearance.GestureTutorial.Feedback"
+        parent="TextAppearance.GestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:letterSpacing">0.03</item>
+        <item name="android:textSize">21sp</item>
+    </style>
+
+    <style name="TextAppearance.GestureTutorial.ButtonLabel"
+        parent="TextAppearance.GestureTutorial.CallToAction">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@color/gesture_tutorial_action_button_label_color</item>
+        <item name="android:letterSpacing">0.02</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textAllCaps">false</item>
+    </style>
+
+    <style name="TextAppearance.GestureTutorial.TextButtonLabel"
+        parent="TextAppearance.GestureTutorial.ButtonLabel">
+        <item name="android:textColor">@color/gesture_tutorial_primary_color</item>
+    </style>
+
+    <!--
+      Can be applied to views to color things like ripples and list highlights the workspace text
+      color.
+    -->
+    <style name="ThemeControlHighlightWorkspaceColor">
+        <item name="android:colorControlHighlight">?attr/workspaceTextColor</item>
+    </style>
+
+    <style name="OverviewActionButton"
+        parent="@android:style/Widget.DeviceDefault.Button.Borderless">
+        <item name="android:textColor">@color/overview_button</item>
+        <item name="android:drawableTint">@color/overview_button</item>
+        <item name="android:tint">?attr/workspaceTextColor</item>
+        <item name="android:drawablePadding">8dp</item>
+        <item name="android:textAllCaps">false</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/xml/overview_file_provider_paths.xml b/quickstep/res/xml/overview_file_provider_paths.xml
new file mode 100644
index 0000000..14d7459
--- /dev/null
+++ b/quickstep/res/xml/overview_file_provider_paths.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <cache-path name="shared_images" path="/" />
+    <files-path name="log_files" path="/" />
+</paths>
\ No newline at end of file
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
new file mode 100644
index 0000000..22d205a
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.quickstep;
+
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.util.DefaultDisplay;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class OrientationTouchTransformerTest {
+    private static final int SIZE_WIDTH = 1080;
+    private static final int SIZE_HEIGHT = 2280;
+    private static final float DENSITY_DISPLAY_METRICS = 3.0f;
+
+    private OrientationTouchTransformer mTouchTransformer;
+
+    Resources mResources;
+    private DefaultDisplay.Info mInfo;
+
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mResources = mock(Resources.class);
+        when(mResources.getBoolean(anyInt())).thenReturn(true);
+        when(mResources.getDimension(anyInt())).thenReturn(10.0f);
+        when(mResources.getDimensionPixelSize(anyInt())).thenReturn(10);
+        DisplayMetrics mockDisplayMetrics = new DisplayMetrics();
+        mockDisplayMetrics.density = DENSITY_DISPLAY_METRICS;
+        when(mResources.getDisplayMetrics()).thenReturn(mockDisplayMetrics);
+        mInfo = createDisplayInfo(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;
+
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        tapAndAssertTrue(100, portraitRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertFalse(100, landscapeRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertTrue(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+
+        // Override region
+        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        tapAndAssertFalse(100, portraitRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertTrue(100, landscapeRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertFalse(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertTrue(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+
+        // Override region again
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        tapAndAssertTrue(100, portraitRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertFalse(100, landscapeRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertTrue(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+    }
+
+    @Test
+    public void enableMultipleRegions_shouldOverrideFirstRegion() {
+        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+
+        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        tapAndAssertFalse(100, portraitRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertTrue(100, landscapeRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertFalse(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertTrue(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        // We have to add 0 rotation second so that gets set as the current rotation, otherwise
+        // matrix transform will fail (tests only work in Portrait at the moment)
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+
+        tapAndAssertTrue(100, portraitRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertFalse(100, landscapeRegionY,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+        tapAndAssertTrue(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+    }
+
+    @Test
+    public void enableMultipleRegions_assistantTriggersInMostRecent() {
+        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        tapAndAssertTrue(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+    }
+
+    @Test
+    public void enableMultipleRegions_assistantTriggersInCurrentOrientationAfterDisable() {
+        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer.enableMultipleRegions(false, mInfo);
+        tapAndAssertTrue(0, portraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, landscapeRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+    }
+
+    @Test
+    public void applyTransform_taskNotFrozen_notInRegion() {
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        tapAndAssertFalse(100, 100,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+    }
+
+    @Test
+    public void applyTransform_taskFrozen_noRotate_outOfRegion() {
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        tapAndAssertFalse(100, 100,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+    }
+
+    @Test
+    public void applyTransform_taskFrozen_noRotate_inRegion() {
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        tapAndAssertTrue(100, y,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+    }
+
+    @Test
+    public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() {
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        float y = generateTouchRegionHeight(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;
+        tapAndAssertTrue(100, y,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
+    }
+
+    @Test
+    @Ignore("There's too much that goes into needing to mock a real motion event so the "
+            + "transforms in native code get applied correctly. Once that happens then maybe we can"
+            + " write slightly more complex unit tests")
+    public void applyTransform_taskNotFrozen_90Rotate_inTwoRegions() {
+        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.enableMultipleRegions(true, mInfo);
+        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        // Landscape point
+        float y1 = generateTouchRegionHeight(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
+        MotionEvent inRegion2 = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, 10);
+        mTouchTransformer.transform(inRegion1_down);
+        mTouchTransformer.transform(inRegion2);
+        assertTrue(mTouchTransformer.touchInValidSwipeRegions(
+                inRegion1_down.getX(), inRegion1_down.getY()));
+        // We only process one gesture region until we see a MotionEvent.ACTION_UP
+        assertFalse(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
+
+        mTouchTransformer.transform(inRegion1_up);
+
+        // Set the new region with this MotionEvent.ACTION_DOWN
+        inRegion2 = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, 10, 370);
+        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
+    }
+
+    private DefaultDisplay.Info createDisplayInfo(int rotation) {
+        Point p = new Point(SIZE_WIDTH, SIZE_HEIGHT);
+        if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
+            p = new Point(SIZE_HEIGHT, SIZE_WIDTH);
+        }
+        return new DefaultDisplay.Info(0, rotation, 0, p, p, p, null);
+    }
+
+    private float generateTouchRegionHeight(int rotation) {
+        float height = SIZE_HEIGHT;
+        if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
+            height = SIZE_WIDTH;
+        }
+        return height - ResourceUtils.DEFAULT_NAVBAR_VALUE * DENSITY_DISPLAY_METRICS;
+    }
+
+    private MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+        return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+    }
+
+    private MotionEvent generateAndTransformMotionEvent(int motionAction, float x, float y) {
+        MotionEvent motionEvent = generateMotionEvent(motionAction, x, y);
+        mTouchTransformer.transform(motionEvent);
+        return motionEvent;
+    }
+
+    private void tapAndAssertTrue(float x, float y, MotionEventAssertion assertion) {
+        MotionEvent motionEvent = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, x, y);
+        assertTrue(assertion.getCondition(motionEvent));
+        generateAndTransformMotionEvent(MotionEvent.ACTION_UP, x, y);
+    }
+
+    private void tapAndAssertFalse(float x, float y, MotionEventAssertion assertion) {
+        MotionEvent motionEvent = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, x, y);
+        assertFalse(assertion.getCondition(motionEvent));
+        generateAndTransformMotionEvent(MotionEvent.ACTION_UP, x, y);
+    }
+
+    private interface MotionEventAssertion {
+        boolean getCondition(MotionEvent motionEvent);
+    }
+}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
new file mode 100644
index 0000000..93b64e6
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.LauncherUIHelper.doLayout;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.util.ReflectionHelpers;
+
+
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class RecentsActivityTest {
+
+    @Test
+    public void testRecentsActivityCreates() {
+        ActivityController<RecentsActivity> controller =
+                Robolectric.buildActivity(RecentsActivity.class);
+
+        RecentsActivity launcher = controller.setup().get();
+        doLayout(launcher);
+
+        // TODO: Ensure that LauncherAppState is not created
+    }
+
+    @Test
+    public void testRecets_showCurrentTask() {
+        ActivityController<RecentsActivity> controller =
+                Robolectric.buildActivity(RecentsActivity.class);
+
+        RecentsActivity activity = controller.setup().get();
+        doLayout(activity);
+
+        FallbackRecentsView frv = activity.getOverviewPanel();
+        frv.showCurrentTask(22);
+        doLayout(activity);
+
+        ThumbnailData thumbnailData = new ThumbnailData();
+        ReflectionHelpers.setField(thumbnailData, "thumbnail",
+                Bitmap.createBitmap(300, 500, Config.ARGB_8888));
+        frv.switchToScreenshot(thumbnailData, () -> { });
+        ShadowLooper.idleMainLooper();
+    }
+}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
new file mode 100644
index 0000000..a31ba21
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.display.DisplayManager;
+import android.view.Surface;
+import android.view.SurfaceControl;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.shadows.LShadowDisplay;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowDisplayManager;
+
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class TaskViewSimulatorTest {
+
+    @Test
+    public void taskProperlyScaled_portrait_noRotation_sameInsets1() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(0, 80, 0, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_portrait_noRotation_sameInsets2() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(55, 80, 55, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets1() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(0, 80, 0, 40))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets2() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(0, 80, 120, 0))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_noRotation_sameInsets3() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(2450, 1250)
+                .withInsets(new Rect(55, 80, 55, 120))
+                .verifyNoTransforms();
+    }
+
+    @Test
+    public void taskProperlyScaled_landscape_rotated() {
+        new TaskMatrixVerifier()
+                .withLauncherSize(1200, 2450)
+                .withInsets(new Rect(0, 80, 0, 120))
+                .withAppBounds(
+                        new Rect(0, 0, 2450, 1200),
+                        new Rect(0, 80, 0, 120),
+                        Surface.ROTATION_90)
+                .verifyNoTransforms();
+    }
+
+    private static class TaskMatrixVerifier extends TransformParams {
+
+        private final Context mContext = RuntimeEnvironment.application;
+
+        private Rect mAppBounds = new Rect();
+        private Rect mLauncherInsets = new Rect();
+
+        private Rect mAppInsets;
+
+        private int mAppRotation = -1;
+        private DeviceProfile mDeviceProfile;
+
+        TaskMatrixVerifier withLauncherSize(int width, int height) {
+            ShadowDisplayManager.changeDisplay(DEFAULT_DISPLAY,
+                    String.format("w%sdp-h%sdp-mdpi", width, height));
+            if (mAppBounds.isEmpty()) {
+                mAppBounds.set(0, 0, width, height);
+            }
+            return this;
+        }
+
+        TaskMatrixVerifier withInsets(Rect insets) {
+            LShadowDisplay shadowDisplay = Shadow.extract(
+                    mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
+            shadowDisplay.setInsets(insets);
+            mLauncherInsets.set(insets);
+            return this;
+        }
+
+        TaskMatrixVerifier withAppBounds(Rect bounds, Rect insets, int appRotation) {
+            mAppBounds.set(bounds);
+            mAppInsets = insets;
+            mAppRotation = appRotation;
+            return this;
+        }
+
+        void verifyNoTransforms() {
+            mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(mContext)
+                    .getDeviceProfile(mContext);
+            mDeviceProfile.updateInsets(mLauncherInsets);
+
+            TaskViewSimulator tvs = new TaskViewSimulator(mContext,
+                    LauncherActivityInterface.INSTANCE);
+            tvs.setDp(mDeviceProfile);
+
+            int launcherRotation = DefaultDisplay.INSTANCE.get(mContext).getInfo().rotation;
+            if (mAppRotation < 0) {
+                mAppRotation = launcherRotation;
+            }
+            tvs.setLayoutRotation(launcherRotation, mAppRotation);
+            if (mAppInsets == null) {
+                mAppInsets = new Rect(mLauncherInsets);
+            }
+            tvs.setPreviewBounds(mAppBounds, mAppInsets);
+
+            tvs.fullScreenProgress.value = 1;
+            tvs.recentsViewScale.value = tvs.getFullScreenScale();
+            tvs.apply(this);
+        }
+
+        @Override
+        public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
+            SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null);
+            proxy.onBuildTargetParams(builder, null, this);
+            return new SurfaceParams[] {builder.build()};
+        }
+
+        @Override
+        public void applySurfaceParams(SurfaceParams[] params) {
+            // Verify that the task position remains the same
+            RectF newAppBounds = new RectF(mAppBounds);
+            params[0].matrix.mapRect(newAppBounds);
+            Assert.assertThat(newAppBounds, new AlmostSame(mAppBounds));
+
+            System.err.println("Bounds mapped: " + mAppBounds + " => " + newAppBounds);
+        }
+    }
+
+    private static class AlmostSame extends TypeSafeMatcher<RectF>  {
+
+        // Allow 1px error margin to account for float to int conversions
+        private final float mError = 1f;
+        private final Rect mExpected;
+
+        AlmostSame(Rect expected) {
+            mExpected = expected;
+        }
+
+        @Override
+        protected boolean matchesSafely(RectF item) {
+            return Math.abs(item.left - mExpected.left) < mError
+                    && Math.abs(item.top - mExpected.top) < mError
+                    && Math.abs(item.right - mExpected.right) < mError
+                    && Math.abs(item.bottom - mExpected.bottom) < mError;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendValue(mExpected);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
new file mode 100644
index 0000000..47ce320
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
+import static com.android.launcher3.LauncherState.FLAG_HIDE_BACK_BUTTON;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
+
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.proxy.ProxyActivityStarter;
+import com.android.launcher3.proxy.StartActivityParams;
+import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.uioverrides.RecentsViewStateController;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.QuickstepOnboardingPrefs;
+import com.android.quickstep.util.RemoteAnimationProvider;
+import com.android.quickstep.util.RemoteFadeOutAnimationListener;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.stream.Stream;
+
+/**
+ * Extension of Launcher activity to provide quickstep specific functionality
+ */
+public abstract class BaseQuickstepLauncher extends Launcher
+        implements NavigationModeChangeListener {
+
+    private DepthController mDepthController = new DepthController(this);
+
+    /**
+     * Reusable command for applying the back button alpha on the background thread.
+     */
+    public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
+            (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(
+                    Float.intBitsToFloat(arg1), arg2 != 0);
+
+    private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
+
+    private OverviewActionsView mActionsView;
+    protected HotseatPredictionController mHotseatPredictionController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
+        addMultiWindowModeChangedListener(mDepthController);
+    }
+
+    @Override
+    public void onDestroy() {
+        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onNavigationModeChanged(Mode newMode) {
+        getDragLayer().recreateControllers();
+        if (mActionsView != null && isOverviewActionsEnabled()) {
+            mActionsView.updateVerticalMargin(newMode);
+        }
+    }
+
+    @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
+        // overview
+        RecentsModel.INSTANCE.get(this).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        RecentsModel.INSTANCE.get(this).onTrimMemory(level);
+    }
+
+    @Override
+    protected void onUiChangedWhileSleeping() {
+        // Remove the snapshot because the content view may have obvious changes.
+        ActivityManagerWrapper.getInstance().invalidateHomeTaskSnapshot(this);
+    }
+
+    @Override
+    public void startIntentSenderForResult(IntentSender intent, int requestCode,
+            Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) {
+        if (requestCode != -1) {
+            mPendingActivityRequestCode = requestCode;
+            StartActivityParams params = new StartActivityParams(this, requestCode);
+            params.intentSender = intent;
+            params.fillInIntent = fillInIntent;
+            params.flagsMask = flagsMask;
+            params.flagsValues = flagsValues;
+            params.extraFlags = extraFlags;
+            params.options = options;
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, params));
+        } else {
+            super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask,
+                    flagsValues, extraFlags, options);
+        }
+    }
+
+    @Override
+    public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
+        if (requestCode != -1) {
+            mPendingActivityRequestCode = requestCode;
+            StartActivityParams params = new StartActivityParams(this, requestCode);
+            params.intent = intent;
+            params.options = options;
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, params));
+        } else {
+            super.startActivityForResult(intent, requestCode, options);
+        }
+    }
+
+    @Override
+    protected void onDeferredResumed() {
+        super.onDeferredResumed();
+        if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
+            // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
+            onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
+            // ProxyActivityStarter is started with clear task to reset the task after which it
+            // removes the task itself.
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, null));
+        }
+    }
+
+    @Override
+    protected void setupViews() {
+        super.setupViews();
+
+        SysUINavigationMode.INSTANCE.get(this).updateMode();
+        mActionsView = findViewById(R.id.overview_actions_view);
+        ((RecentsView) getOverviewPanel()).init(mActionsView);
+
+        if (isOverviewActionsEnabled()) {
+            // Overview is above all other launcher elements, including qsb, so move it to the top.
+            getOverviewPanel().bringToFront();
+            mActionsView.bringToFront();
+            mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
+        }
+    }
+
+    private boolean isOverviewActionsEnabled() {
+        return FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this);
+    }
+
+    public <T extends OverviewActionsView> T getActionsView() {
+        return (T) mActionsView;
+    }
+
+    @Override
+    protected void closeOpenViews(boolean animate) {
+        super.closeOpenViews(animate);
+        ActivityManagerWrapper.getInstance()
+                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+    }
+
+    @Override
+    protected StateHandler<LauncherState>[] createStateHandlers() {
+        return new StateHandler[] {
+                getAllAppsController(),
+                getWorkspace(),
+                getDepthController(),
+                new RecentsViewStateController(this),
+                new BackButtonAlphaHandler(this)};
+    }
+
+    public DepthController getDepthController() {
+        return mDepthController;
+    }
+
+    @Override
+    protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
+        return new QuickstepOnboardingPrefs(this, sharedPrefs);
+    }
+
+    @Override
+    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
+        QuickstepAppTransitionManagerImpl appTransitionManager =
+                (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
+        appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
+            @Override
+            public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
+                    RemoteAnimationTargetCompat[] wallpaperTargets) {
+
+                // On the first call clear the reference.
+                signal.cancel();
+
+                ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
+                fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets,
+                        wallpaperTargets));
+                AnimatorSet anim = new AnimatorSet();
+                anim.play(fadeAnimation);
+                return anim;
+            }
+        }, signal);
+    }
+
+    @Override
+    public float[] getNormalOverviewScaleAndOffset() {
+        return SysUINavigationMode.getMode(this) == Mode.NO_BUTTON
+                ? new float[] {1, 1} : new float[] {1.1f, 0};
+    }
+
+    @Override
+    public void onDragLayerHierarchyChanged() {
+        onLauncherStateOrFocusChanged();
+    }
+
+    @Override
+    protected void onActivityFlagsChanged(int changeBits) {
+        if ((changeBits
+                & (ACTIVITY_STATE_WINDOW_FOCUSED | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+            onLauncherStateOrFocusChanged();
+        }
+
+        if ((changeBits & ACTIVITY_STATE_STARTED) != 0) {
+            mDepthController.setActivityStarted(isStarted());
+        }
+
+        super.onActivityFlagsChanged(changeBits);
+    }
+
+    public boolean shouldBackButtonBeHidden(LauncherState toState) {
+        Mode mode = SysUINavigationMode.getMode(this);
+        boolean shouldBackButtonBeHidden = mode.hasGestures
+                && toState.hasFlag(FLAG_HIDE_BACK_BUTTON)
+                && hasWindowFocus()
+                && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0;
+        if (shouldBackButtonBeHidden) {
+            // Show the back button if there is a floating view visible.
+            shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(this,
+                    TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null;
+        }
+        return shouldBackButtonBeHidden;
+    }
+
+    /**
+     * Sets the back button visibility based on the current state/window focus.
+     */
+    private void onLauncherStateOrFocusChanged() {
+        boolean shouldBackButtonBeHidden = shouldBackButtonBeHidden(getStateManager().getState());
+        UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
+                shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+        if (getDragLayer() != null) {
+            getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
+        }
+    }
+
+    @Override
+    public void finishBindingItems(int pageBoundFirst) {
+        super.finishBindingItems(pageBoundFirst);
+        // Instantiate and initialize WellbeingModel now that its loading won't interfere with
+        // populating workspace.
+        // TODO: Find a better place for this
+        WellbeingModel.INSTANCE.get(this);
+    }
+
+    @Override
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.concat(super.getSupportedShortcuts(),
+                Stream.of(WellbeingModel.SHORTCUT_FACTORY));
+    }
+
+    public ShelfPeekAnim getShelfPeekAnim() {
+        return mShelfPeekAnim;
+    }
+
+    /**
+     * Returns Prediction controller for hybrid hotseat
+     */
+    public HotseatPredictionController getHotseatPredictionController() {
+        return mHotseatPredictionController;
+    }
+
+    public void setHintUserWillBeActive() {
+        addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index a8e2956..02769c6 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -17,8 +17,7 @@
 
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.systemui.shared.recents.utilities.Utilities
-        .postAtFrontOfQueueAsynchronously;
+import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -28,14 +27,17 @@
 import android.os.Build;
 import android.os.Handler;
 
-import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
 import androidx.annotation.BinderThread;
 import androidx.annotation.UiThread;
 
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
 @TargetApi(Build.VERSION_CODES.P)
-public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
+public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat,
+        WrappedAnimationRunnerImpl {
+
+    private static final String TAG = "LauncherAnimationRunner";
 
     private final Handler mHandler;
     private final boolean mStartAtFrontOfQueue;
@@ -50,13 +52,21 @@
         mStartAtFrontOfQueue = startAtFrontOfQueue;
     }
 
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    // Called only in R+ platform
     @BinderThread
-    @Override
-    public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) {
+    public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) {
         Runnable r = () -> {
             finishExistingAnimation();
-            mAnimationResult = new AnimationResult(runnable);
-            onCreateAnimation(targetCompats, mAnimationResult);
+            mAnimationResult = new AnimationResult(() -> {
+                runnable.run();
+                mAnimationResult = null;
+            });
+            onCreateAnimation(appTargets, wallpaperTargets, mAnimationResult);
         };
         if (mStartAtFrontOfQueue) {
             postAtFrontOfQueueAsynchronously(mHandler, r);
@@ -65,13 +75,21 @@
         }
     }
 
+    // Called only in Q platform
+    @BinderThread
+    @Deprecated
+    public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets, Runnable runnable) {
+        onAnimationStart(appTargets, new RemoteAnimationTargetCompat[0], runnable);
+    }
+
     /**
      * Called on the UI thread when the animation targets are received. The implementation must
      * call {@link AnimationResult#setAnimation} with the target animation to be run.
      */
     @UiThread
     public abstract void onCreateAnimation(
-            RemoteAnimationTargetCompat[] targetCompats, AnimationResult result);
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result);
 
     @UiThread
     private void finishExistingAnimation() {
@@ -135,8 +153,9 @@
 
                 // Because t=0 has the app icon in its original spot, we can skip the
                 // first frame and have the same movement one frame earlier.
-                mAnimator.setCurrentPlayTime(getSingleFrameMs(context));
+                mAnimator.setCurrentPlayTime(
+                        Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index b9ce1ce..7fb0d43 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -15,34 +15,36 @@
  */
 package com.android.launcher3;
 
+import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import com.android.launcher3.states.InternalStateHandler;
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
-import com.android.quickstep.OverviewCallbacks;
+import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.BiPredicate;
 
 @TargetApi(Build.VERSION_CODES.P)
-public class LauncherInitListener extends InternalStateHandler implements ActivityInitListener {
-
-    private final BiPredicate<Launcher, Boolean> mOnInitListener;
+public class LauncherInitListener extends ActivityInitListener<Launcher> {
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
 
+    /**
+     * @param onInitListener a callback made when the activity is initialized. The callback should
+     *                       return true to continue receiving callbacks (ie. for if the activity is
+     *                       recreated).
+     */
     public LauncherInitListener(BiPredicate<Launcher, Boolean> onInitListener) {
-        mOnInitListener = onInitListener;
+        super(onInitListener, Launcher.ACTIVITY_TRACKER);
     }
 
     @Override
-    protected boolean init(Launcher launcher, boolean alreadyOnHome) {
+    public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
         if (mRemoteAnimationProvider != null) {
             QuickstepAppTransitionManagerImpl appTransitionManager =
                     (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
@@ -50,42 +52,37 @@
             // Set a one-time animation provider. After the first call, this will get cleared.
             // TODO: Probably also check the intended target id.
             CancellationSignal cancellationSignal = new CancellationSignal();
-            appTransitionManager.setRemoteAnimationProvider((targets) -> {
+            appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
+                @Override
+                public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
+                        RemoteAnimationTargetCompat[] wallpaperTargets) {
 
-                // On the first call clear the reference.
-                cancellationSignal.cancel();
-                RemoteAnimationProvider provider = mRemoteAnimationProvider;
-                mRemoteAnimationProvider = null;
+                    // On the first call clear the reference.
+                    cancellationSignal.cancel();
+                    RemoteAnimationProvider provider = mRemoteAnimationProvider;
+                    mRemoteAnimationProvider = null;
 
-                if (provider != null && launcher.getStateManager().getState().overviewUi) {
-                    return provider.createWindowAnimation(targets);
+                    if (provider != null && launcher.getStateManager().getState().overviewUi) {
+                        return provider.createWindowAnimation(appTargets, wallpaperTargets);
+                    }
+                    return null;
                 }
-                return null;
             }, cancellationSignal);
         }
-        OverviewCallbacks.get(launcher).onInitOverviewTransition();
-        return mOnInitListener.test(launcher, alreadyOnHome);
-    }
-
-    @Override
-    public void register() {
-        initWhenReady();
+        launcher.deferOverlayCallbacksUntilNextResumeOrStop();
+        return super.handleInit(launcher, alreadyOnHome);
     }
 
     @Override
     public void unregister() {
         mRemoteAnimationProvider = null;
-        clearReference();
+        super.unregister();
     }
 
     @Override
     public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
             Context context, Handler handler, long duration) {
         mRemoteAnimationProvider = animProvider;
-
-        register();
-
-        Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
-        context.startActivity(addToIntent(new Intent((intent))), options);
+        super.registerAndStartActivity(intent, animProvider, context, handler, duration);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index a91410c..10f789d 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -16,11 +16,14 @@
 
 package com.android.launcher3;
 
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
 import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
@@ -28,7 +31,10 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
 import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
+import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
@@ -47,6 +53,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Matrix;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
@@ -55,6 +62,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Pair;
+import android.util.TypedValue;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -62,17 +70,19 @@
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -80,13 +90,12 @@
 import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
 /**
  * {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
- * home and/or all-apps.
+ * home and/or all-apps.  Not used for 3p launchers.
  */
 @TargetApi(Build.VERSION_CODES.O)
 @SuppressWarnings("unused")
@@ -135,7 +144,7 @@
     // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down.
     public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f;
 
-    protected final Launcher mLauncher;
+    protected final BaseQuickstepLauncher mLauncher;
 
     private final DragLayer mDragLayer;
     private final AlphaProperty mDragLayerAlpha;
@@ -150,8 +159,10 @@
     private DeviceProfile mDeviceProfile;
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
-
-    private final ShelfPeekAnim mShelfPeekAnim;
+    // Strong refs to runners which are cleared when the launcher activity is destroyed
+    private WrappedAnimationRunnerImpl mWallpaperOpenRunner;
+    private WrappedAnimationRunnerImpl mAppLaunchRunner;
+    private WrappedAnimationRunnerImpl mKeyguardGoingAwayRunner;
 
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
         @Override
@@ -166,7 +177,7 @@
     };
 
     public QuickstepAppTransitionManagerImpl(Context context) {
-        mLauncher = Launcher.getLauncher(context);
+        mLauncher = Launcher.cast(Launcher.getLauncher(context));
         mDragLayer = mLauncher.getDragLayer();
         mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS);
         mHandler = new Handler(Looper.getMainLooper());
@@ -179,13 +190,6 @@
         mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
 
         mLauncher.addOnDeviceProfileChangeListener(this);
-        registerRemoteAnimations();
-
-        mShelfPeekAnim = new ShelfPeekAnim(mLauncher);
-    }
-
-    public ShelfPeekAnim getShelfPeekAnim() {
-        return mShelfPeekAnim;
     }
 
     @Override
@@ -207,30 +211,9 @@
     public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
         if (hasControlRemoteAppTransitionPermission()) {
             boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
-            RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler,
-                    true /* startAtFrontOfQueue */) {
-
-                @Override
-                public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
-                        AnimationResult result) {
-                    AnimatorSet anim = new AnimatorSet();
-
-                    boolean launcherClosing =
-                            launcherIsATargetWithMode(targetCompats, MODE_CLOSING);
-
-                    if (isLaunchingFromRecents(v, targetCompats)) {
-                        composeRecentsLaunchAnimator(anim, v, targetCompats, launcherClosing);
-                    } else {
-                        composeIconLaunchAnimator(anim, v, targetCompats, launcherClosing);
-                    }
-
-                    if (launcherClosing) {
-                        anim.addListener(mForceInvisibleListener);
-                    }
-
-                    result.setAnimation(anim, mLauncher);
-                }
-            };
+            mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v);
+            RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
+                    mAppLaunchRunner, true /* startAtFrontOfQueue */);
 
             // Note that this duration is a guess as we do not know if the animation will be a
             // recents launch or not for sure until we know the opening app targets.
@@ -264,36 +247,39 @@
      *
      * @param anim the animator set to add to
      * @param v the launching view
-     * @param targets the apps that are opening/closing
+     * @param appTargets the apps that are opening/closing
      * @param launcherClosing true if the launcher app is closing
      */
     protected abstract void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
-            @NonNull RemoteAnimationTargetCompat[] targets, boolean launcherClosing);
+            @NonNull RemoteAnimationTargetCompat[] appTargets,
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing);
 
     /**
      * Compose the animations for a launch from the app icon.
      *
      * @param anim the animation to add to
      * @param v the launching view with the icon
-     * @param targets the list of opening/closing apps
+     * @param appTargets the list of opening/closing apps
      * @param launcherClosing true if launcher is closing
      */
     private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
-            @NonNull RemoteAnimationTargetCompat[] targets, boolean launcherClosing) {
+            @NonNull RemoteAnimationTargetCompat[] appTargets,
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+            boolean launcherClosing) {
         // Set the state animation first so that any state listeners are called
         // before our internal listeners.
         mLauncher.getStateManager().setCurrentAnimation(anim);
 
-        Rect windowTargetBounds = getWindowTargetBounds(targets);
+        Rect windowTargetBounds = getWindowTargetBounds(appTargets);
         boolean isAllOpeningTargetTrs = true;
-        for (int i = 0; i < targets.length; i++) {
-            RemoteAnimationTargetCompat target = targets[i];
+        for (int i = 0; i < appTargets.length; i++) {
+            RemoteAnimationTargetCompat target = appTargets[i];
             if (target.mode == MODE_OPENING) {
                 isAllOpeningTargetTrs &= target.isTranslucent;
             }
             if (!isAllOpeningTargetTrs) break;
         }
-        anim.play(getOpeningWindowAnimators(v, targets, windowTargetBounds,
+        anim.play(getOpeningWindowAnimators(v, appTargets, wallpaperTargets, windowTargetBounds,
                 !isAllOpeningTargetTrs));
         if (launcherClosing) {
             Pair<AnimatorSet, Runnable> launcherContentAnimator =
@@ -306,6 +292,15 @@
                     launcherContentAnimator.second.run();
                 }
             });
+        } else {
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    mLauncher.addOnResumeCallback(() ->
+                            ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+                            mLauncher.getStateManager().getState().getDepth(mLauncher)).start());
+                }
+            });
         }
     }
 
@@ -314,13 +309,17 @@
      * In multiwindow mode, we need to get the final size of the opening app window target to help
      * figure out where the floating view should animate to.
      */
-    private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] targets) {
+    private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] appTargets) {
         Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx);
         if (mLauncher.isInMultiWindowMode()) {
-            for (RemoteAnimationTargetCompat target : targets) {
+            for (RemoteAnimationTargetCompat target : appTargets) {
                 if (target.mode == MODE_OPENING) {
-                    bounds.set(target.sourceContainerBounds);
-                    bounds.offsetTo(target.position.x, target.position.y);
+                    bounds.set(target.screenSpaceBounds);
+                    if (target.localBounds != null) {
+                        bounds.set(target.localBounds);
+                    } else {
+                        bounds.offsetTo(target.position.x, target.position.y);
+                    }
                     return bounds;
                 }
             }
@@ -397,18 +396,35 @@
             alpha.setInterpolator(LINEAR);
             launcherAnimator.play(alpha);
 
-            mDragLayer.setTranslationY(trans[0]);
-            ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans);
-            transY.setInterpolator(AGGRESSIVE_EASE);
-            transY.setDuration(CONTENT_TRANSLATION_DURATION);
-            launcherAnimator.play(transY);
+            Workspace workspace = mLauncher.getWorkspace();
+            View currentPage = ((CellLayout) workspace.getChildAt(workspace.getCurrentPage()))
+                    .getShortcutsAndWidgets();
+            View hotseat = mLauncher.getHotseat();
+            View qsb = mLauncher.findViewById(R.id.search_container_all_apps);
 
-            mDragLayer.getScrim().hideSysUiScrim(true);
+            currentPage.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            qsb.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+            launcherAnimator.play(ObjectAnimator.ofFloat(currentPage, View.TRANSLATION_Y, trans));
+            launcherAnimator.play(ObjectAnimator.ofFloat(hotseat, View.TRANSLATION_Y, trans));
+            launcherAnimator.play(ObjectAnimator.ofFloat(qsb, View.TRANSLATION_Y, trans));
+
             // Pause page indicator animations as they lead to layer trashing.
             mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
-            mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
 
-            endListener = this::resetContentView;
+            endListener = () -> {
+                currentPage.setTranslationY(0);
+                hotseat.setTranslationY(0);
+                qsb.setTranslationY(0);
+
+                currentPage.setLayerType(View.LAYER_TYPE_NONE, null);
+                hotseat.setLayerType(View.LAYER_TYPE_NONE, null);
+                qsb.setLayerType(View.LAYER_TYPE_NONE, null);
+
+                mDragLayerAlpha.setValue(1f);
+                mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
+            };
         }
         return new Pair<>(launcherAnimator, endListener);
     }
@@ -425,9 +441,11 @@
             float[] alphas, float[] trans);
 
     /**
-     * @return Animator that controls the window of the opening targets.
+     * @return Animator that controls the window of the opening targets from app icons.
      */
-    private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets,
+    private Animator getOpeningWindowAnimators(View v,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
             Rect windowTargetBounds, boolean toggleVisibility) {
         RectF bounds = new RectF();
         FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v,
@@ -435,11 +453,11 @@
         Rect crop = new Rect();
         Matrix matrix = new Matrix();
 
-        RemoteAnimationTargetSet openingTargets = new RemoteAnimationTargetSet(targets,
-                MODE_OPENING);
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(floatingView);
-        openingTargets.addDependentTransactionApplier(surfaceApplier);
+        RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+                wallpaperTargets, MODE_OPENING);
+        SurfaceTransactionApplier surfaceApplier =
+                new SurfaceTransactionApplier(floatingView);
+        openingTargets.addReleaseCheck(surfaceApplier);
 
         // Scale the app icon to take up the entire screen. This simplifies the math when
         // animating the app window position / scale.
@@ -476,9 +494,11 @@
                 : APP_LAUNCH_ALPHA_DOWN_DURATION;
 
         RectF targetBounds = new RectF(windowTargetBounds);
-        RectF currentBounds = new RectF();
+        RectF iconBounds = new RectF();
         RectF temp = new RectF();
+        Point tmpPos = new Point();
 
+        AnimatorSet animatorSet = new AnimatorSet();
         ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
         appAnimator.setDuration(APP_LAUNCH_DURATION);
         appAnimator.setInterpolator(LINEAR);
@@ -512,7 +532,7 @@
         appAnimator.addUpdateListener(new MultiValueUpdateListener() {
             FloatProp mDx = new FloatProp(0, dX, 0, xDuration, AGGRESSIVE_EASE);
             FloatProp mDy = new FloatProp(0, dY, 0, yDuration, AGGRESSIVE_EASE);
-            FloatProp mIconScale = new FloatProp(initialStartScale, scale, 0, APP_LAUNCH_DURATION,
+            FloatProp mScale = new FloatProp(initialStartScale, scale, 0, APP_LAUNCH_DURATION,
                     EXAGGERATED_EASE);
             FloatProp mIconAlpha = new FloatProp(1f, 0f, APP_LAUNCH_ALPHA_START_DELAY,
                     alphaDuration, LINEAR);
@@ -523,95 +543,160 @@
 
             @Override
             public void onUpdate(float percent) {
-                // Calculate app icon size.
-                float iconWidth = bounds.width() * mIconScale.value;
-                float iconHeight = bounds.height() * mIconScale.value;
+                // Calculate the size.
+                float width = bounds.width() * mScale.value;
+                float height = bounds.height() * mScale.value;
 
-                // Animate the window crop so that it starts off as a square.
-                final int windowWidth;
-                final int windowHeight;
+                // Animate the crop so that it starts off as a square.
+                final int cropWidth;
+                final int cropHeight;
                 if (mDeviceProfile.isVerticalBarLayout()) {
-                    windowWidth = (int) mCroppedSize.value;
-                    windowHeight = windowTargetBounds.height();
+                    cropWidth = (int) mCroppedSize.value;
+                    cropHeight = windowTargetBounds.height();
                 } else {
-                    windowWidth = windowTargetBounds.width();
-                    windowHeight = (int) mCroppedSize.value;
+                    cropWidth = windowTargetBounds.width();
+                    cropHeight = (int) mCroppedSize.value;
                 }
-                crop.set(0, 0, windowWidth, windowHeight);
+                crop.set(0, 0, cropWidth, cropHeight);
 
-                // Scale the app window to match the icon size.
-                float scaleX = iconWidth / windowWidth;
-                float scaleY = iconHeight / windowHeight;
+                // Scale the size to match the crop.
+                float scaleX = width / cropWidth;
+                float scaleY = height / cropHeight;
                 float scale = Math.min(1f, Math.max(scaleX, scaleY));
 
-                float scaledWindowWidth = windowWidth * scale;
-                float scaledWindowHeight = windowHeight * scale;
+                float scaledCropWidth = cropWidth * scale;
+                float scaledCropHeight = cropHeight * scale;
+                float offsetX  = (scaledCropWidth - width) / 2;
+                float offsetY = (scaledCropHeight - height) / 2;
 
-                float offsetX = (scaledWindowWidth - iconWidth) / 2;
-                float offsetY = (scaledWindowHeight - iconHeight) / 2;
-
-                // Calculate the window position
+                // Calculate the window position.
                 temp.set(bounds);
                 temp.offset(dragLayerBounds[0], dragLayerBounds[1]);
                 temp.offset(mDx.value, mDy.value);
-                Utilities.scaleRectFAboutCenter(temp, mIconScale.value);
-                float transX0 = temp.left - offsetX;
-                float transY0 = temp.top - offsetY;
+                Utilities.scaleRectFAboutCenter(temp, mScale.value);
+                float windowTransX0 = temp.left - offsetX;
+                float windowTransY0 = temp.top - offsetY;
+
+                // Calculate the icon position.
+                iconBounds.set(bounds);
+                iconBounds.offset(mDx.value, mDy.value);
+                Utilities.scaleRectFAboutCenter(iconBounds, mScale.value);
+                iconBounds.left -= offsetX;
+                iconBounds.top -= offsetY;
+                iconBounds.right += offsetX;
+                iconBounds.bottom += offsetY;
 
                 float croppedHeight = (windowTargetBounds.height() - crop.height()) * scale;
                 float croppedWidth = (windowTargetBounds.width() - crop.width()) * scale;
-                SurfaceParams[] params = new SurfaceParams[targets.length];
-                for (int i = targets.length - 1; i >= 0; i--) {
-                    RemoteAnimationTargetCompat target = targets[i];
-                    Rect targetCrop;
-                    final float alpha;
-                    final float cornerRadius;
+                SurfaceParams[] params = new SurfaceParams[appTargets.length];
+                for (int i = appTargets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = appTargets[i];
+                    SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+
                     if (target.mode == MODE_OPENING) {
                         matrix.setScale(scale, scale);
-                        matrix.postTranslate(transX0, transY0);
-                        targetCrop = crop;
-                        alpha = 1f - mIconAlpha.value;
-                        cornerRadius = mWindowRadius.value;
-                        matrix.mapRect(currentBounds, targetBounds);
-                        if (mDeviceProfile.isVerticalBarLayout()) {
-                            currentBounds.right -= croppedWidth;
-                        } else {
-                            currentBounds.bottom -= croppedHeight;
-                        }
-                        floatingView.update(currentBounds, mIconAlpha.value, percent, 0f,
-                                cornerRadius * scale, true /* isOpening */);
-                    } else {
-                        matrix.setTranslate(target.position.x, target.position.y);
-                        targetCrop = target.sourceContainerBounds;
-                        alpha = 1f;
-                        cornerRadius = 0;
-                    }
+                        matrix.postTranslate(windowTransX0, windowTransY0);
 
-                    params[i] = new SurfaceParams(target.leash, alpha, matrix, targetCrop,
-                            RemoteAnimationProvider.getLayer(target, MODE_OPENING),
-                            cornerRadius);
+                        floatingView.update(iconBounds, mIconAlpha.value, percent, 0f,
+                                mWindowRadius.value * scale, true /* isOpening */);
+                        builder.withMatrix(matrix)
+                                .withWindowCrop(crop)
+                                .withAlpha(1f - mIconAlpha.value)
+                                .withCornerRadius(mWindowRadius.value);
+                    } else {
+                        tmpPos.set(target.position.x, target.position.y);
+                        if (target.localBounds != null) {
+                            final Rect localBounds = target.localBounds;
+                            tmpPos.set(target.localBounds.left, target.localBounds.top);
+                        }
+
+                        matrix.setTranslate(tmpPos.x, tmpPos.y);
+                        final Rect crop = new Rect(target.screenSpaceBounds);
+                        crop.offsetTo(0, 0);
+                        builder.withMatrix(matrix)
+                                .withWindowCrop(crop)
+                                .withAlpha(1f);
+                    }
+                    params[i] = builder.build();
                 }
                 surfaceApplier.scheduleApply(params);
             }
         });
-        return appAnimator;
+
+        // When launching an app from overview that doesn't map to a task, we still want to just
+        // blur the wallpaper instead of the launcher surface as well
+        boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
+        DepthController depthController = mLauncher.getDepthController();
+        ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController, DEPTH,
+                BACKGROUND_APP.getDepth(mLauncher))
+                .setDuration(APP_LAUNCH_DURATION);
+        if (allowBlurringLauncher) {
+            depthController.setSurfaceToApp(RemoteAnimationProvider.findLowestOpaqueLayerTarget(
+                    appTargets, MODE_OPENING));
+            backgroundRadiusAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    depthController.setSurfaceToApp(null);
+                }
+            });
+        }
+
+        animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
+        return animatorSet;
     }
 
     /**
      * Registers remote animations used when closing apps to home screen.
      */
-    private void registerRemoteAnimations() {
-        // Unregister this
+    @Override
+    public void registerRemoteAnimations() {
+        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+            return;
+        }
         if (hasControlRemoteAppTransitionPermission()) {
+            mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+
             RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
             definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
                     WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
-                    new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(false /* fromUnlock */),
+                    new RemoteAnimationAdapterCompat(
+                            new WrappedLauncherAnimationRunner<>(mWallpaperOpenRunner,
+                                    false /* startAtFrontOfQueue */),
                             CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
+
+            if (KEYGUARD_ANIMATION.get()) {
+                mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
+                definition.addRemoteAnimation(
+                        WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+                        new RemoteAnimationAdapterCompat(
+                                new WrappedLauncherAnimationRunner<>(mKeyguardGoingAwayRunner,
+                                        true /* startAtFrontOfQueue */),
+                                CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
+            }
+
             new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
         }
     }
 
+    /**
+     * Unregisters all remote animations.
+     */
+    @Override
+    public void unregisterRemoteAnimations() {
+        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+            return;
+        }
+        if (hasControlRemoteAppTransitionPermission()) {
+            new ActivityCompat(mLauncher).unregisterRemoteAnimations();
+
+            // Also clear strong references to the runners registered with the remote animation
+            // definition so we don't have to wait for the system gc
+            mWallpaperOpenRunner = null;
+            mAppLaunchRunner = null;
+            mKeyguardGoingAwayRunner = null;
+        }
+    }
+
     private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
         return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode);
     }
@@ -620,17 +705,16 @@
      * @return Runner that plays when user goes to Launcher
      *         ie. pressing home, swiping up from nav bar.
      */
-    RemoteAnimationRunnerCompat getWallpaperOpenRunner(boolean fromUnlock) {
-        return new WallpaperOpenLauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */,
-                fromUnlock);
+    WrappedAnimationRunnerImpl createWallpaperOpenRunner(boolean fromUnlock) {
+        return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
     }
 
     /**
      * Animator that controls the transformations of the windows when unlocking the device.
      */
-    private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] targets) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(mDragLayer);
+    private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets) {
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
         ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
         unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
         float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 :
@@ -638,12 +722,14 @@
         unlockAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
-                SurfaceParams[] params = new SurfaceParams[targets.length];
-                for (int i = targets.length - 1; i >= 0; i--) {
-                    RemoteAnimationTargetCompat target = targets[i];
-                    params[i] = new SurfaceParams(target.leash, 1f, null,
-                            target.sourceContainerBounds,
-                            RemoteAnimationProvider.getLayer(target, MODE_OPENING), cornerRadius);
+                SurfaceParams[] params = new SurfaceParams[appTargets.length];
+                for (int i = appTargets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = appTargets[i];
+                    params[i] = new SurfaceParams.Builder(target.leash)
+                            .withAlpha(1f)
+                            .withWindowCrop(target.screenSpaceBounds)
+                            .withCornerRadius(cornerRadius)
+                            .build();
                 }
                 surfaceApplier.scheduleApply(params);
             }
@@ -654,10 +740,11 @@
     /**
      * Animator that controls the transformations of the windows the targets that are closing.
      */
-    private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(mDragLayer);
+    private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets) {
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
         Matrix matrix = new Matrix();
+        Point tmpPos = new Point();
         ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
         int duration = CLOSING_TRANSITION_DURATION_MS;
         float windowCornerRadius = mDeviceProfile.isMultiWindowMode
@@ -670,28 +757,35 @@
 
             @Override
             public void onUpdate(float percent) {
-                SurfaceParams[] params = new SurfaceParams[targets.length];
-                for (int i = targets.length - 1; i >= 0; i--) {
-                    RemoteAnimationTargetCompat target = targets[i];
-                    final float alpha;
-                    final float cornerRadius;
+                SurfaceParams[] params = new SurfaceParams[appTargets.length];
+                for (int i = appTargets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = appTargets[i];
+                    SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+
+                    tmpPos.set(target.position.x, target.position.y);
+                    if (target.localBounds != null) {
+                        tmpPos.set(target.localBounds.left, target.localBounds.top);
+                    }
+
                     if (target.mode == MODE_CLOSING) {
                         matrix.setScale(mScale.value, mScale.value,
-                                target.sourceContainerBounds.centerX(),
-                                target.sourceContainerBounds.centerY());
+                                target.screenSpaceBounds.centerX(),
+                                target.screenSpaceBounds.centerY());
                         matrix.postTranslate(0, mDy.value);
-                        matrix.postTranslate(target.position.x, target.position.y);
-                        alpha = mAlpha.value;
-                        cornerRadius = windowCornerRadius;
+                        matrix.postTranslate(tmpPos.x, tmpPos.y);
+                        builder.withMatrix(matrix)
+                                .withAlpha(mAlpha.value)
+                                .withCornerRadius(windowCornerRadius);
                     } else {
-                        matrix.setTranslate(target.position.x, target.position.y);
-                        alpha = 1f;
-                        cornerRadius = 0f;
+                        matrix.setTranslate(tmpPos.x, tmpPos.y);
+                        builder.withMatrix(matrix)
+                                .withAlpha(1f);
                     }
-                    params[i] = new SurfaceParams(target.leash, alpha, matrix,
-                            target.sourceContainerBounds,
-                            RemoteAnimationProvider.getLayer(target, MODE_CLOSING),
-                            cornerRadius);
+                    final Rect crop = new Rect(target.screenSpaceBounds);
+                    crop.offsetTo(0, 0);
+                    params[i] = builder
+                            .withWindowCrop(crop)
+                            .build();
                 }
                 surfaceApplier.scheduleApply(params);
             }
@@ -700,61 +794,6 @@
         return closingAnimator;
     }
 
-    /**
-     * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}.
-     */
-    private void createLauncherResumeAnimation(AnimatorSet anim) {
-        if (mLauncher.isInState(LauncherState.ALL_APPS)) {
-            Pair<AnimatorSet, Runnable> contentAnimator =
-                    getLauncherContentAnimator(false /* isAppOpening */,
-                            new float[] {-mContentTransY, 0});
-            contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY);
-            anim.play(contentAnimator.first);
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    contentAnimator.second.run();
-                }
-            });
-        } else {
-            AnimatorSet workspaceAnimator = new AnimatorSet();
-
-            mDragLayer.setTranslationY(-mWorkspaceTransY);;
-            workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y,
-                    -mWorkspaceTransY, 0));
-
-            mDragLayerAlpha.setValue(0);
-            workspaceAnimator.play(ObjectAnimator.ofFloat(
-                    mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f));
-
-            workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
-            workspaceAnimator.setDuration(333);
-            workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7);
-
-            mDragLayer.getScrim().hideSysUiScrim(true);
-
-            // Pause page indicator animations as they lead to layer trashing.
-            mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
-            mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-
-            workspaceAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    resetContentView();
-                }
-            });
-            anim.play(workspaceAnimator);
-        }
-    }
-
-    private void resetContentView() {
-        mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
-        mDragLayerAlpha.setValue(1f);
-        mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null);
-        mDragLayer.setTranslationY(0f);
-        mDragLayer.getScrim().hideSysUiScrim(false);
-    }
-
     private boolean hasControlRemoteAppTransitionPermission() {
         return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
                 == PackageManager.PERMISSION_GRANTED;
@@ -763,23 +802,37 @@
     /**
      * Remote animation runner for animation from the app to Launcher, including recents.
      */
-    class WallpaperOpenLauncherAnimationRunner extends LauncherAnimationRunner {
+    protected class WallpaperOpenLauncherAnimationRunner implements WrappedAnimationRunnerImpl {
+
+        private final Handler mHandler;
         private final boolean mFromUnlock;
 
-        public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue,
-                boolean fromUnlock) {
-            super(handler, startAtFrontOfQueue);
+        public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
+            mHandler = handler;
             mFromUnlock = fromUnlock;
         }
 
         @Override
-        public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+        @Override
+        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+                RemoteAnimationTargetCompat[] wallpaperTargets,
                 LauncherAnimationRunner.AnimationResult result) {
+            if (mLauncher.isDestroyed()) {
+                AnimatorSet anim = new AnimatorSet();
+                anim.play(getClosingWindowAnimators(appTargets, wallpaperTargets));
+                result.setAnimation(anim, mLauncher.getApplicationContext());
+                return;
+            }
+
             if (!mLauncher.hasBeenResumed()) {
                 // If launcher is not resumed, wait until new async-frame after resume
                 mLauncher.addOnResumeCallback(() ->
                         postAsyncCallback(mHandler, () ->
-                                onCreateAnimation(targetCompats, result)));
+                                onCreateAnimation(appTargets, wallpaperTargets, result)));
                 return;
             }
 
@@ -791,14 +844,14 @@
             AnimatorSet anim = null;
             RemoteAnimationProvider provider = mRemoteAnimationProvider;
             if (provider != null) {
-                anim = provider.createWindowAnimation(targetCompats);
+                anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
             }
 
             if (anim == null) {
                 anim = new AnimatorSet();
                 anim.play(mFromUnlock
-                        ? getUnlockWindowAnimator(targetCompats)
-                        : getClosingWindowAnimators(targetCompats));
+                        ? getUnlockWindowAnimator(appTargets, wallpaperTargets)
+                        : getClosingWindowAnimators(appTargets, wallpaperTargets));
 
                 // Normally, we run the launcher content animation when we are transitioning
                 // home, but if home is already visible, then we don't want to animate the
@@ -808,15 +861,16 @@
                 // targets list because it is already visible). In that case, we force
                 // invisibility on touch down, and only reset it after the animation to home
                 // is initialized.
-                if (launcherIsATargetWithMode(targetCompats, MODE_OPENING)
+                if (launcherIsATargetWithMode(appTargets, MODE_OPENING)
                         || mLauncher.isForceInvisible()) {
                     // Only register the content animation for cancellation when state changes
                     mLauncher.getStateManager().setCurrentAnimation(anim);
-                    if (mFromUnlock) {
+
+                    if (mLauncher.isInState(LauncherState.ALL_APPS)) {
                         Pair<AnimatorSet, Runnable> contentAnimator =
                                 getLauncherContentAnimator(false /* isAppOpening */,
-                                        new float[] {mContentTransY, 0});
-                        contentAnimator.first.setStartDelay(0);
+                                        new float[] {-mContentTransY, 0});
+                        contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY);
                         anim.play(contentAnimator.first);
                         anim.addListener(new AnimatorListenerAdapter() {
                             @Override
@@ -825,7 +879,12 @@
                             }
                         });
                     } else {
-                        createLauncherResumeAnimation(anim);
+                        float velocityDpPerS = DynamicResource.provider(mLauncher)
+                                .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+                        float velocityPxPerS = TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+                                velocityDpPerS, mLauncher.getResources().getDisplayMetrics());
+                        anim.play(new StaggeredWorkspaceAnim(mLauncher, velocityPxPerS, false)
+                                .getAnimators());
                     }
                 }
             }
@@ -834,4 +893,47 @@
             result.setAnimation(anim, mLauncher);
         }
     }
+
+    /**
+     * Remote animation runner for animation to launch an app.
+     */
+    private class AppLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
+
+        private final Handler mHandler;
+        private final View mV;
+
+        AppLaunchAnimationRunner(Handler handler, View v) {
+            mHandler = handler;
+            mV = v;
+        }
+
+        @Override
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+        @Override
+        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+                RemoteAnimationTargetCompat[] wallpaperTargets,
+                LauncherAnimationRunner.AnimationResult result) {
+            AnimatorSet anim = new AnimatorSet();
+
+            boolean launcherClosing =
+                    launcherIsATargetWithMode(appTargets, MODE_CLOSING);
+
+            if (isLaunchingFromRecents(mV, appTargets)) {
+                composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+                        launcherClosing);
+            } else {
+                composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+                        launcherClosing);
+            }
+
+            if (launcherClosing) {
+                anim.addListener(mForceInvisibleListener);
+            }
+
+            result.setAnimation(anim, mLauncher);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
new file mode 100644
index 0000000..da2aee4
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+import android.os.Handler;
+
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * Used with WrappedLauncherAnimationRunner as an interface for the runner to call back to the
+ * implementation.
+ */
+public interface WrappedAnimationRunnerImpl {
+    Handler getHandler();
+    void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            LauncherAnimationRunner.AnimationResult result);
+}
diff --git a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
new file mode 100644
index 0000000..1753b62
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class is needed to wrap any animation runner that is a part of the
+ * RemoteAnimationDefinition:
+ * - Launcher creates a new instance of the LauncherAppTransitionManagerImpl whenever it is
+ *   created, which in turn registers a new definition
+ * - When the definition is registered, window manager retains a strong binder reference to the
+ *   runner passed in
+ * - If the Launcher activity is recreated, the new definition registered will replace the old
+ *   reference in the system's activity record, but until the system server is GC'd, the binder
+ *   reference will still exist, which references the runner in the Launcher process, which
+ *   references the (old) Launcher activity through this class
+ *
+ * Instead we make the runner provided to the definition static only holding a weak reference to
+ * the runner implementation.  When this animation manager is destroyed, we remove the Launcher
+ * reference to the runner, leaving only the weak ref from the runner.
+ */
+public class WrappedLauncherAnimationRunner<R extends WrappedAnimationRunnerImpl>
+        extends LauncherAnimationRunner {
+    private WeakReference<R> mImpl;
+
+    public WrappedLauncherAnimationRunner(R animationRunnerImpl, boolean startAtFrontOfQueue) {
+        super(animationRunnerImpl.getHandler(), startAtFrontOfQueue);
+        mImpl = new WeakReference<>(animationRunnerImpl);
+    }
+
+    @Override
+    public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+        R animationRunnerImpl = mImpl.get();
+        if (animationRunnerImpl != null) {
+            animationRunnerImpl.onCreateAnimation(appTargets, wallpaperTargets, result);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
new file mode 100644
index 0000000..f42b124
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -0,0 +1,336 @@
+/*
+ * 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.model;
+
+import static android.content.ContentResolver.SCHEME_CONTENT;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import android.annotation.TargetApi;
+import android.app.RemoteAction;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.LauncherApps;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.RemoteActionShortcut;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data model for digital wellbeing status of apps.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public final class WellbeingModel {
+    private static final String TAG = "WellbeingModel";
+    private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
+    private static final boolean DEBUG = false;
+
+    private static final int MSG_PACKAGE_ADDED = 1;
+    private static final int MSG_PACKAGE_REMOVED = 2;
+    private static final int MSG_FULL_REFRESH = 3;
+
+    // Welbeing contract
+    private static final String METHOD_GET_ACTIONS = "get_actions";
+    private static final String EXTRA_ACTIONS = "actions";
+    private static final String EXTRA_ACTION = "action";
+    private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
+    private static final String EXTRA_PACKAGES = "packages";
+    private static final String EXTRA_SUCCESS = "success";
+
+    public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
+            new MainThreadInitializedObject<>(WellbeingModel::new);
+
+    private final Context mContext;
+    private final String mWellbeingProviderPkg;
+    private final Handler mWorkerHandler;
+
+    private final ContentObserver mContentObserver;
+
+    private final Object mModelLock = new Object();
+    // Maps the action Id to the corresponding RemoteAction
+    private final Map<String, RemoteAction> mActionIdMap = new ArrayMap<>();
+    private final Map<String, String> mPackageToActionId = new HashMap<>();
+
+    private boolean mIsInTest;
+
+    private WellbeingModel(final Context context) {
+        mContext = context;
+        mWorkerHandler =
+                new Handler(createAndStartNewLooper("WellbeingHandler"), this::handleMessage);
+
+        mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
+        mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                // Wellbeing reports that app actions have changed.
+                if (DEBUG || mIsInTest) {
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
+                            + "], uri = [" + uri + "]");
+                }
+                Preconditions.assertUIThread();
+                updateWellbeingData();
+            }
+        };
+
+        if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
+            context.registerReceiver(
+                    new SimpleBroadcastReceiver(this::onWellbeingProviderChanged),
+                    PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg,
+                            Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
+                            Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
+                            Intent.ACTION_PACKAGE_RESTARTED));
+
+            IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+            filter.addDataScheme("package");
+            context.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
+                    filter);
+
+            restartObserver();
+        }
+    }
+
+    public void setInTest(boolean inTest) {
+        mIsInTest = inTest;
+    }
+
+    protected void onWellbeingProviderChanged(Intent intent) {
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "Changes to Wellbeing package: intent = [" + intent + "]");
+        }
+        restartObserver();
+    }
+
+    private void restartObserver() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        resolver.unregisterContentObserver(mContentObserver);
+        Uri actionsUri = apiBuilder().path("actions").build();
+        try {
+            resolver.registerContentObserver(
+                    actionsUri, true /* notifyForDescendants */, mContentObserver);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        updateWellbeingData();
+    }
+
+    @MainThread
+    private SystemShortcut getShortcutForApp(String packageName, int userId,
+            BaseDraggingActivity activity, ItemInfo info) {
+        Preconditions.assertUIThread();
+        // Work profile apps are not recognized by digital wellbeing.
+        if (userId != UserHandle.myUserId()) {
+            if (DEBUG || mIsInTest) {
+                Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user");
+            }
+            return null;
+        }
+
+        synchronized (mModelLock) {
+            String actionId = mPackageToActionId.get(packageName);
+            final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null;
+            if (action == null) {
+                if (DEBUG || mIsInTest) {
+                    Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action");
+                }
+                return null;
+            }
+            if (DEBUG || mIsInTest) {
+                Log.d(TAG,
+                        "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle()
+                                + "'");
+            }
+            return new RemoteActionShortcut(action, activity, info);
+        }
+    }
+
+    private void updateWellbeingData() {
+        mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
+    }
+
+    private Uri.Builder apiBuilder() {
+        return new Uri.Builder()
+                .scheme(SCHEME_CONTENT)
+                .authority(mWellbeingProviderPkg + ".api");
+    }
+
+    private boolean updateActions(String... packageNames) {
+        if (packageNames.length == 0) {
+            return true;
+        }
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "retrieveActions() called with: packageNames = [" + String.join(", ",
+                    packageNames) + "]");
+        }
+        Preconditions.assertNonUiThread();
+
+        Uri contentUri = apiBuilder().build();
+        final Bundle remoteActionBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            if (client == null) {
+                if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): null provider");
+                return false;
+            }
+
+            // Prepare wellbeing call parameters.
+            final Bundle params = new Bundle();
+            params.putStringArray(EXTRA_PACKAGES, packageNames);
+            params.putInt(EXTRA_MAX_NUM_ACTIONS_SHOWN, 1);
+            // Perform wellbeing call .
+            remoteActionBundle = client.call(METHOD_GET_ACTIONS, null, params);
+            if (!remoteActionBundle.getBoolean(EXTRA_SUCCESS, true)) return false;
+
+            synchronized (mModelLock) {
+                // Remove the entries for requested packages, and then update the fist with what we
+                // got from service
+                Arrays.stream(packageNames).forEach(mPackageToActionId::remove);
+
+                // The result consists of sub-bundles, each one is per a remote action. Each
+                // sub-bundle has a RemoteAction and a list of packages to which the action applies.
+                for (String actionId :
+                        remoteActionBundle.getStringArray(EXTRA_ACTIONS)) {
+                    final Bundle actionBundle = remoteActionBundle.getBundle(actionId);
+                    mActionIdMap.put(actionId,
+                            actionBundle.getParcelable(EXTRA_ACTION));
+
+                    final String[] packagesForAction =
+                            actionBundle.getStringArray(EXTRA_PACKAGES);
+                    if (DEBUG || mIsInTest) {
+                        Log.d(TAG, "....actionId: " + actionId + ", packages: " + String.join(", ",
+                                packagesForAction));
+                    }
+                    for (String packageName : packagesForAction) {
+                        mPackageToActionId.put(packageName, actionId);
+                    }
+                }
+            }
+        } catch (DeadObjectException e) {
+            Log.i(TAG, "retrieveActions(): DeadObjectException");
+            return false;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+            return true;
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): finished");
+        return true;
+    }
+
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_PACKAGE_REMOVED: {
+                String packageName = (String) msg.obj;
+                mWorkerHandler.removeCallbacksAndMessages(packageName);
+                synchronized (mModelLock) {
+                    mPackageToActionId.remove(packageName);
+                }
+                return true;
+            }
+            case MSG_PACKAGE_ADDED: {
+                String packageName = (String) msg.obj;
+                mWorkerHandler.removeCallbacksAndMessages(packageName);
+                if (!updateActions(packageName)) {
+                    scheduleRefreshRetry(msg);
+                }
+                return true;
+            }
+
+            case MSG_FULL_REFRESH: {
+                // Remove all existing messages
+                mWorkerHandler.removeCallbacksAndMessages(null);
+                final String[] packageNames = mContext.getSystemService(LauncherApps.class)
+                        .getActivityList(null, Process.myUserHandle()).stream()
+                        .map(li -> li.getApplicationInfo().packageName).distinct()
+                        .toArray(String[]::new);
+                if (!updateActions(packageNames)) {
+                    scheduleRefreshRetry(msg);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void scheduleRefreshRetry(Message originalMsg) {
+        int retryCount = originalMsg.arg1;
+        if (retryCount >= RETRY_TIMES_MS.length) {
+            // To many retries, skip
+            return;
+        }
+
+        Message msg = Message.obtain(originalMsg);
+        msg.arg1 = retryCount + 1;
+        mWorkerHandler.sendMessageDelayed(msg, RETRY_TIMES_MS[retryCount]);
+    }
+
+    private void onAppPackageChanged(Intent intent) {
+        if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
+        Preconditions.assertUIThread();
+
+        final String packageName = intent.getData().getSchemeSpecificPart();
+        if (packageName == null || packageName.length() == 0) {
+            // they sent us a bad intent
+            return;
+        }
+
+        final String action = intent.getAction();
+        if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+            Message.obtain(mWorkerHandler, MSG_PACKAGE_REMOVED, packageName).sendToTarget();
+        } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            Message.obtain(mWorkerHandler, MSG_PACKAGE_ADDED, packageName).sendToTarget();
+        }
+    }
+
+    /**
+     * Shortcut factory for generating wellbeing action
+     */
+    public static final SystemShortcut.Factory SHORTCUT_FACTORY =
+            (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity)
+                    .getShortcutForApp(
+                            info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
+                            activity, info);
+}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
new file mode 100644
index 0000000..13501a4
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
@@ -0,0 +1,70 @@
+/*
+ * 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.statehandlers;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.AnimatedFloat.VALUE;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SystemUiProxy;
+
+/**
+ * State handler for animating back button alpha
+ */
+public class BackButtonAlphaHandler implements StateHandler<LauncherState> {
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final AnimatedFloat mBackAlpha = new AnimatedFloat(this::updateBackAlpha);
+
+    public BackButtonAlphaHandler(BaseQuickstepLauncher launcher) {
+        mLauncher = launcher;
+    }
+
+    @Override
+    public void setState(LauncherState state) { }
+
+    @Override
+    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+            PendingAnimation animation) {
+        if (config.onlyPlayAtomicComponent()) {
+            return;
+        }
+
+        if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
+            // If the nav mode is not gestural, then force back button alpha to be 1
+            UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
+                    BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
+            return;
+        }
+
+        mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastBackButtonAlpha();
+        animation.setFloat(mBackAlpha, VALUE,
+                mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR);
+    }
+
+    private void updateBackAlpha() {
+        UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
+                BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, mBackAlpha.value, false /* animate */);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
new file mode 100644
index 0000000..fe8f0c6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -0,0 +1,260 @@
+/*
+ * 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.statehandlers;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.os.IBinder;
+import android.util.FloatProperty;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.systemui.shared.system.BlurUtils;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SurfaceControlCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+import com.android.systemui.shared.system.WallpaperManagerCompat;
+
+/**
+ * Controls blur and wallpaper zoom, for the Launcher surface only.
+ */
+public class DepthController implements StateHandler<LauncherState>,
+        BaseActivity.MultiWindowModeChangedListener {
+
+    public static final FloatProperty<DepthController> DEPTH =
+            new FloatProperty<DepthController>("depth") {
+                @Override
+                public void setValue(DepthController depthController, float depth) {
+                    depthController.setDepth(depth);
+                }
+
+                @Override
+                public Float get(DepthController depthController) {
+                    return depthController.mDepth;
+                }
+            };
+
+    /**
+     * A property that updates the background blur within a given range of values (ie. even if the
+     * animator goes beyond 0..1, the interpolated value will still be bounded).
+     */
+    public static class ClampedDepthProperty extends FloatProperty<DepthController> {
+        private final float mMinValue;
+        private final float mMaxValue;
+
+        public ClampedDepthProperty(float minValue, float maxValue) {
+            super("depthClamped");
+            mMinValue = minValue;
+            mMaxValue = maxValue;
+        }
+
+        @Override
+        public void setValue(DepthController depthController, float depth) {
+            depthController.setDepth(Utilities.boundToRange(depth, mMinValue, mMaxValue));
+        }
+
+        @Override
+        public Float get(DepthController depthController) {
+            return depthController.mDepth;
+        }
+    }
+
+    private final ViewTreeObserver.OnDrawListener mOnDrawListener =
+            new ViewTreeObserver.OnDrawListener() {
+                @Override
+                public void onDraw() {
+                    View view = mLauncher.getDragLayer();
+                    setSurface(new SurfaceControlCompat(view));
+                    view.post(() -> view.getViewTreeObserver().removeOnDrawListener(this));
+                }
+            };
+
+    private final Launcher mLauncher;
+    /**
+     * Blur radius when completely zoomed out, in pixels.
+     */
+    private int mMaxBlurRadius;
+    private WallpaperManagerCompat mWallpaperManager;
+    private SurfaceControlCompat mSurface;
+    /**
+     * Ratio from 0 to 1, where 0 is fully zoomed out, and 1 is zoomed in.
+     * @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float)
+     */
+    private float mDepth;
+
+    // Workaround for animating the depth when multiwindow mode changes.
+    private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false;
+
+    private View.OnAttachStateChangeListener mOnAttachListener;
+
+    public DepthController(Launcher l) {
+        mLauncher = l;
+    }
+
+    private void ensureDependencies() {
+        if (mWallpaperManager == null) {
+            mMaxBlurRadius = mLauncher.getResources().getInteger(R.integer.max_depth_blur_radius);
+            mWallpaperManager = new WallpaperManagerCompat(mLauncher);
+        }
+        if (mLauncher.getRootView() != null && mOnAttachListener == null) {
+            mOnAttachListener = new View.OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View view) {
+                    // To handle the case where window token is invalid during last setDepth call.
+                    IBinder windowToken = mLauncher.getRootView().getWindowToken();
+                    if (windowToken != null) {
+                        mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth);
+                    }
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View view) {
+                }
+            };
+            mLauncher.getRootView().addOnAttachStateChangeListener(mOnAttachListener);
+        }
+    }
+
+    /**
+     * Sets if the underlying activity is started or not
+     */
+    public void setActivityStarted(boolean isStarted) {
+        if (isStarted) {
+            mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+        } else {
+            mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+            setSurface(null);
+        }
+    }
+
+    /**
+     * Sets the specified app target surface to apply the blur to.
+     */
+    public void setSurfaceToApp(RemoteAnimationTargetCompat target) {
+        if (target != null) {
+            setSurface(target.leash);
+        } else {
+            setActivityStarted(mLauncher.isStarted());
+        }
+    }
+
+    private void setSurface(SurfaceControlCompat surface) {
+        if (mSurface != surface) {
+            mSurface = surface;
+            if (surface != null) {
+                setDepth(mDepth);
+            } else {
+                // If there is no surface, then reset the ratio
+                setDepth(0f);
+            }
+        }
+    }
+
+    @Override
+    public void setState(LauncherState toState) {
+        if (mSurface == null || mIgnoreStateChangesDuringMultiWindowAnimation) {
+            return;
+        }
+
+        float toDepth = toState.getDepth(mLauncher);
+        if (Float.compare(mDepth, toDepth) != 0) {
+            setDepth(toDepth);
+        }
+    }
+
+    @Override
+    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+            PendingAnimation animation) {
+        if (mSurface == null
+                || config.onlyPlayAtomicComponent()
+                || config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
+                || mIgnoreStateChangesDuringMultiWindowAnimation) {
+            return;
+        }
+
+        float toDepth = toState.getDepth(mLauncher);
+        if (Float.compare(mDepth, toDepth) != 0) {
+            animation.setFloat(this, DEPTH, toDepth, config.getInterpolator(ANIM_DEPTH, LINEAR));
+        }
+    }
+
+    private void setDepth(float depth) {
+        depth = Utilities.boundToRange(depth, 0, 1);
+        // Round out the depth to dedupe frequent, non-perceptable updates
+        int depthI = (int) (depth * 256);
+        float depthF = depthI / 256f;
+        if (Float.compare(mDepth, depthF) == 0) {
+            return;
+        }
+
+        boolean supportsBlur = BlurUtils.supportsBlursOnWindows();
+        if (supportsBlur && (mSurface == null || !mSurface.isValid())) {
+            return;
+        }
+        mDepth = depthF;
+        ensureDependencies();
+        IBinder windowToken = mLauncher.getRootView().getWindowToken();
+        if (windowToken != null) {
+            mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth);
+        }
+
+        if (supportsBlur) {
+            final int blur;
+            if (mLauncher.isInState(LauncherState.ALL_APPS) && mDepth == 1) {
+                // All apps has a solid background. We don't need to draw blurs after it's fully
+                // visible. This will take us out of GPU composition, saving battery and increasing
+                // performance.
+                blur = 0;
+            } else {
+                blur = (int) (mDepth * mMaxBlurRadius);
+            }
+            new TransactionCompat()
+                    .setBackgroundBlurRadius(mSurface, blur)
+                    .apply();
+        }
+    }
+
+    @Override
+    public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+        mIgnoreStateChangesDuringMultiWindowAnimation = true;
+
+        ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(this, DEPTH,
+                mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode))
+                .setDuration(300);
+        mwAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIgnoreStateChangesDuringMultiWindowAnimation = false;
+            }
+        });
+        mwAnimation.setAutoCancel(true);
+        mwAnimation.start();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
new file mode 100644
index 0000000..1e03b05
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -0,0 +1,30 @@
+/*
+ * 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.uioverrides;
+
+import android.app.Person;
+import android.content.pm.ShortcutInfo;
+
+import com.android.launcher3.Utilities;
+
+public class ApiWrapper {
+
+    public static Person[] getPersons(ShortcutInfo si) {
+        Person[] persons = si.getPersons();
+        return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
deleted file mode 100644
index 693ae60..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.quickstep.OverviewInteractionState;
-
-public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler {
-
-    private static final String TAG = "BackButtonAlphaHandler";
-
-    private final Launcher mLauncher;
-    private final OverviewInteractionState mOverviewInteractionState;
-
-    public BackButtonAlphaHandler(Launcher launcher) {
-        mLauncher = launcher;
-        mOverviewInteractionState = OverviewInteractionState.INSTANCE.get(mLauncher);
-    }
-
-    @Override
-    public void setState(LauncherState state) {
-        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-    }
-
-    @Override
-    public void setStateWithAnimation(LauncherState toState,
-            AnimatorSetBuilder builder, LauncherStateManager.AnimationConfig config) {
-        if (!config.playNonAtomicComponent()) {
-            return;
-        }
-        float fromAlpha = mOverviewInteractionState.getBackButtonAlpha();
-        float toAlpha = toState.hideBackButton ? 0 : 1;
-        if (Float.compare(fromAlpha, toAlpha) != 0) {
-            ValueAnimator anim = ValueAnimator.ofFloat(fromAlpha, toAlpha);
-            anim.setDuration(config.duration);
-            anim.addUpdateListener(valueAnimator -> {
-                final float alpha = (float) valueAnimator.getAnimatedValue();
-                mOverviewInteractionState.setBackButtonAlpha(alpha, false);
-            });
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    // Reapply the final alpha in case some state (e.g. window focus) changed.
-                    UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-                }
-            });
-            builder.play(anim);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 174e49b..ec3a490 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -17,30 +17,30 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCRIM_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.anim.AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
+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 android.util.FloatProperty;
-import android.view.View;
-import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * State handler for recents view. Manages UI changes and animations for recents view based off the
@@ -48,78 +48,70 @@
  *
  * @param <T> the recents view
  */
-public abstract class BaseRecentsViewStateController<T extends View>
-        implements StateHandler {
+public abstract class BaseRecentsViewStateController<T extends RecentsView>
+        implements StateHandler<LauncherState> {
     protected final T mRecentsView;
-    protected final Launcher mLauncher;
+    protected final BaseQuickstepLauncher mLauncher;
 
-    public BaseRecentsViewStateController(@NonNull Launcher launcher) {
+    public BaseRecentsViewStateController(@NonNull BaseQuickstepLauncher launcher) {
         mLauncher = launcher;
         mRecentsView = launcher.getOverviewPanel();
     }
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        ScaleAndTranslation scaleAndTranslation = state
-                .getOverviewScaleAndTranslation(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleAndTranslation.scale);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        mRecentsView.setTranslationX(translationX);
-        mRecentsView.setTranslationY(scaleAndTranslation.translationY);
+        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
+        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
+
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
         SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
+        getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
     }
 
     @Override
-    public final void setStateWithAnimation(@NonNull final LauncherState toState,
-            @NonNull AnimatorSetBuilder builder, @NonNull AnimationConfig config) {
-        boolean playAtomicOverviewComponent = config.playAtomicOverviewScaleComponent()
-                || config.playAtomicOverviewPeekComponent();
-        if (!playAtomicOverviewComponent) {
+    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+            PendingAnimation builder) {
+        if (!config.hasAnimationFlag(PLAY_ATOMIC_OVERVIEW_PEEK | PLAY_ATOMIC_OVERVIEW_SCALE)) {
             // The entire recents animation is played atomically.
             return;
         }
-        if (builder.hasFlag(FLAG_DONT_ANIMATE_OVERVIEW)) {
+        if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
             return;
         }
-        setStateWithAnimationInternal(toState, builder, config);
+        setStateWithAnimationInternal(toState, config, builder);
     }
 
     /**
      * Core logic for animating the recents view UI.
      *
      * @param toState state to animate to
-     * @param builder animator set builder
      * @param config current animation config
+     * @param setter animator set builder
      */
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
-            @NonNull AnimatorSetBuilder builder, @NonNull AnimationConfig config) {
-        PropertySetter setter = config.getPropertySetter(builder);
-        ScaleAndTranslation scaleAndTranslation = toState.getOverviewScaleAndTranslation(mLauncher);
-        Interpolator scaleInterpolator = builder.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndTranslation.scale, scaleInterpolator);
-        Interpolator translateXInterpolator = builder.getInterpolator(
-                ANIM_OVERVIEW_TRANSLATE_X, LINEAR);
-        Interpolator translateYInterpolator = builder.getInterpolator(
-                ANIM_OVERVIEW_TRANSLATE_Y, LINEAR);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        setter.setFloat(mRecentsView, View.TRANSLATION_X, translationX, translateXInterpolator);
-        setter.setFloat(mRecentsView, View.TRANSLATION_Y, scaleAndTranslation.translationY,
-                translateYInterpolator);
+            @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+                config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
+        setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
+                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
-                builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
+                config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
         setter.setFloat(scrim, SCRIM_PROGRESS, toState.getOverviewScrimAlpha(mLauncher),
-                builder.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
+                config.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
+
+        setter.setFloat(
+                mRecentsView, getTaskModalnessProperty(),
+                toState.getOverviewModalness(),
+                config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
     }
 
+    abstract FloatProperty getTaskModalnessProperty();
+
     /**
      * Get property for content alpha for the recents view.
      *
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
new file mode 100644
index 0000000..d8aa235
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides;
+
+import static android.os.IBinder.FLAG_ONEWAY;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+/**
+ * A binder proxy transaction listener for tracking non-whitelisted binder calls.
+ */
+public class DejankBinderTracker implements Binder.ProxyTransactListener {
+    private static final String TAG = "DejankBinderTracker";
+
+    private static final Object sLock = new Object();
+    private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>();
+    static {
+        // Common IPCs that are ok to block the main thread.
+        sWhitelistedFrameworkClasses.add("android.view.IWindowSession");
+        sWhitelistedFrameworkClasses.add("android.os.IPowerManager");
+    }
+    private static boolean sTemporarilyIgnoreTracking = false;
+
+    // Used by the client to limit binder tracking to specific regions
+    private static boolean sTrackingAllowed = false;
+
+    private BiConsumer<String, Integer> mUnexpectedTransactionCallback;
+    private boolean mIsTracking = false;
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}.
+     */
+    @MainThread
+    public static void whitelistIpcs(Runnable runnable) {
+        sTemporarilyIgnoreTracking = true;
+        runnable.run();
+        sTemporarilyIgnoreTracking = false;
+    }
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+     */
+    @MainThread
+    public static <T> T whitelistIpcs(Supplier<T> supplier) {
+        sTemporarilyIgnoreTracking = true;
+        T value = supplier.get();
+        sTemporarilyIgnoreTracking = false;
+        return value;
+    }
+
+    /**
+     * Enables binder tracking during a test.
+     */
+    @MainThread
+    public static void allowBinderTrackingInTests() {
+        sTrackingAllowed = true;
+    }
+
+    /**
+     * Disables binder tracking during a test.
+     */
+    @MainThread
+    public static void disallowBinderTrackingInTests() {
+        sTrackingAllowed = false;
+    }
+
+    public DejankBinderTracker(BiConsumer<String, Integer> unexpectedTransactionCallback) {
+        mUnexpectedTransactionCallback = unexpectedTransactionCallback;
+    }
+
+    @MainThread
+    public void startTracking() {
+        if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
+                && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) {
+            Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception());
+            return;
+        }
+        if (mIsTracking) {
+            return;
+        }
+        mIsTracking = true;
+        Binder.setProxyTransactListener(this);
+    }
+
+    @MainThread
+    public void stopTracking() {
+        if (!mIsTracking) {
+            return;
+        }
+        mIsTracking = false;
+        Binder.setProxyTransactListener(null);
+    }
+
+    // Override the hidden Binder#onTransactStarted method
+    public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
+        if (!mIsTracking
+                || !sTrackingAllowed
+                || sTemporarilyIgnoreTracking
+                || (flags & FLAG_ONEWAY) == FLAG_ONEWAY
+                || !isMainThread()) {
+            return null;
+        }
+
+        String descriptor;
+        try {
+            descriptor = binder.getInterfaceDescriptor();
+            if (sWhitelistedFrameworkClasses.contains(descriptor)) {
+                return null;
+            }
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            descriptor = binder.getClass().getSimpleName();
+        }
+
+        mUnexpectedTransactionCallback.accept(descriptor, transactionCode);
+        return null;
+    }
+
+    @Override
+    public Object onTransactStarted(IBinder binder, int transactionCode) {
+        // Do nothing
+        return null;
+    }
+
+    @Override
+    public void onTransactEnded(Object session) {
+        // Do nothing
+    }
+
+    public static boolean isMainThread() {
+        return Thread.currentThread() == Looper.getMainLooper().getThread();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
new file mode 100644
index 0000000..010694b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
+
+import java.util.ArrayList;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class DeviceFlag extends DebugFlag {
+
+    public static final String NAMESPACE_LAUNCHER = "launcher";
+
+    private final boolean mDefaultValueInCode;
+    ArrayList<Runnable> mListeners;
+
+    public DeviceFlag(String key, boolean defaultValue, String description) {
+        super(key, getDeviceValue(key, defaultValue), description);
+        mDefaultValueInCode = defaultValue;
+    }
+
+    @Override
+    protected StringBuilder appendProps(StringBuilder src) {
+        return super.appendProps(src).append(", mDefaultValueInCode=").append(mDefaultValueInCode);
+    }
+
+    @Override
+    public void initialize(Context context) {
+        super.initialize(context);
+        if (mListeners == null) {
+            mListeners = new ArrayList<>();
+            registerDeviceConfigChangedListener(context);
+        }
+    }
+
+    @Override
+    public void addChangeListener(Context context, Runnable r) {
+        if (mListeners == null) {
+            initialize(context);
+        }
+        mListeners.add(r);
+    }
+
+    private void registerDeviceConfigChangedListener(Context context) {
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_LAUNCHER,
+                context.getMainExecutor(),
+                properties -> {
+                    if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())
+                            || !properties.getKeyset().contains(key)) {
+                        return;
+                    }
+                    defaultValue = getDeviceValue(key, mDefaultValueInCode);
+                    initialize(context);
+                    for (Runnable r: mListeners) {
+                        r.run();
+                    }
+                });
+    }
+
+    protected static boolean getDeviceValue(String key, boolean defaultValue) {
+        return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValue);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java b/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java
deleted file mode 100644
index 2d9a161..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import android.content.Context;
-import android.os.Handler;
-
-import com.android.systemui.shared.system.RotationWatcher;
-
-/**
- * Utility class for listening for rotation changes
- */
-public class DisplayRotationListener extends RotationWatcher {
-
-    private final Runnable mCallback;
-    private Handler mHandler;
-
-    public DisplayRotationListener(Context context, Runnable callback) {
-        super(context);
-        mCallback = callback;
-    }
-
-    @Override
-    public void enable() {
-        if (mHandler == null) {
-            mHandler = new Handler();
-        }
-        super.enable();
-    }
-
-    @Override
-    protected void onRotationChanged(int i) {
-        mHandler.post(mCallback);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java b/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
deleted file mode 100644
index 853a1c6..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import android.content.Context;
-import android.provider.DeviceConfig;
-import com.android.launcher3.config.BaseFlags.BaseTogglableFlag;
-
-public class TogglableFlag extends BaseTogglableFlag {
-    public static final String NAMESPACE_LAUNCHER = "launcher";
-    public static final String TAG = "TogglableFlag";
-
-    public TogglableFlag(String key, boolean defaultValue, String description) {
-        super(key, defaultValue, description);
-    }
-
-    @Override
-    public boolean getOverridenDefaultValue(boolean value) {
-        return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, getKey(), value);
-    }
-
-    @Override
-    public void addChangeListener(Context context, Runnable r) {
-        DeviceConfig.addOnPropertiesChangedListener(
-            NAMESPACE_LAUNCHER,
-            context.getMainExecutor(),
-            (properties) -> {
-                if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())) {
-                    return;
-                }
-                initialize(context);
-                r.run();
-            });
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
deleted file mode 100644
index 0790cf6..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ /dev/null
@@ -1,262 +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.uioverrides;
-
-import static android.app.Activity.RESULT_CANCELED;
-
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
-import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.Activity;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.content.pm.ShortcutInfo;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.util.Base64;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.proxy.ProxyActivityStarter;
-import com.android.launcher3.proxy.StartActivityParams;
-import com.android.quickstep.OverviewInteractionState;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
-import com.android.quickstep.util.RemoteFadeOutAnimationListener;
-import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
-import java.util.zip.Deflater;
-
-public class UiFactory extends RecentsUiFactory {
-
-    public static Runnable enableLiveUIChanges(Launcher launcher) {
-        NavigationModeChangeListener listener = m -> {
-            launcher.getDragLayer().recreateControllers();
-            launcher.getRotationHelper().setRotationHadDifferentUI(m != Mode.NO_BUTTON);
-        };
-        SysUINavigationMode mode = SysUINavigationMode.INSTANCE.get(launcher);
-        SysUINavigationMode.Mode m = mode.addModeChangeListener(listener);
-        launcher.getRotationHelper().setRotationHadDifferentUI(m != Mode.NO_BUTTON);
-        return () -> mode.removeModeChangeListener(listener);
-    }
-
-    public static StateHandler[] getStateHandler(Launcher launcher) {
-        return new StateHandler[] {
-                launcher.getAllAppsController(),
-                launcher.getWorkspace(),
-                createRecentsViewStateController(launcher),
-                new BackButtonAlphaHandler(launcher)};
-    }
-
-    /**
-     * Sets the back button visibility based on the current state/window focus.
-     */
-    public static void onLauncherStateOrFocusChanged(Launcher launcher) {
-        boolean shouldBackButtonBeHidden = launcher != null
-                && launcher.getStateManager().getState().hideBackButton
-                && launcher.hasWindowFocus();
-        if (shouldBackButtonBeHidden) {
-            // Show the back button if there is a floating view visible.
-            shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher,
-                    TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null;
-        }
-        OverviewInteractionState.INSTANCE.get(launcher)
-                .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
-        if (launcher != null && launcher.getDragLayer() != null) {
-            launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
-        }
-    }
-
-    public static void onCreate(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) {
-                }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE.get(launcher).getMode()
-                            .hasGestures;
-                    LauncherState prevState = launcher.getStateManager().getLastState();
-
-                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
-                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT <=
-                            launcher.getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
-                        launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-                        launcher.getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-
-        if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
-            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) {
-                }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    LauncherState prevState = launcher.getStateManager().getLastState();
-
-                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT <=
-                            launcher.getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
-                        launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
-                        launcher.getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-    }
-
-    public static void onEnterAnimationComplete(Context context) {
-        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
-        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
-        // overview
-        RecentsModel.INSTANCE.get(context).getThumbnailCache()
-                .getHighResLoadingState().setVisible(true);
-    }
-
-    public static void onTrimMemory(Context context, int level) {
-        RecentsModel model = RecentsModel.INSTANCE.get(context);
-        if (model != null) {
-            model.onTrimMemory(level);
-        }
-    }
-
-    public static void useFadeOutAnimationForLauncherStart(Launcher launcher,
-            CancellationSignal cancellationSignal) {
-        QuickstepAppTransitionManagerImpl appTransitionManager =
-                (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
-        appTransitionManager.setRemoteAnimationProvider((targets) -> {
-
-            // On the first call clear the reference.
-            cancellationSignal.cancel();
-
-            ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
-            fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(targets));
-            AnimatorSet anim = new AnimatorSet();
-            anim.play(fadeAnimation);
-            return anim;
-        }, cancellationSignal);
-    }
-
-    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
-        if (!Utilities.IS_DEBUG_DEVICE) {
-            return false;
-        }
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) {
-            return false;
-        }
-
-        Deflater deflater = new Deflater();
-        deflater.setInput(out.toByteArray());
-        deflater.finish();
-
-        out.reset();
-        byte[] buffer = new byte[1024];
-        while (!deflater.finished()) {
-            int count = deflater.deflate(buffer); // returns the generated code... index
-            out.write(buffer, 0, count);
-        }
-
-        writer.println("--encoded-view-dump-v0--");
-        writer.println(Base64.encodeToString(
-                out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING));
-        return true;
-    }
-
-    public static boolean startIntentSenderForResult(Activity activity, IntentSender intent,
-            int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
-            Bundle options) {
-        StartActivityParams params = new StartActivityParams(activity, requestCode);
-        params.intentSender = intent;
-        params.fillInIntent = fillInIntent;
-        params.flagsMask = flagsMask;
-        params.flagsValues = flagsValues;
-        params.extraFlags = extraFlags;
-        params.options = options;
-        ((Context) activity).startActivity(ProxyActivityStarter.getLaunchIntent(activity, params));
-        return true;
-    }
-
-    public static boolean startActivityForResult(Activity activity, Intent intent, int requestCode,
-            Bundle options) {
-        StartActivityParams params = new StartActivityParams(activity, requestCode);
-        params.intent = intent;
-        params.options = options;
-        activity.startActivity(ProxyActivityStarter.getLaunchIntent(activity, params));
-        return true;
-    }
-
-    /**
-     * Removes any active ProxyActivityStarter task and sends RESULT_CANCELED to Launcher.
-     *
-     * ProxyActivityStarter is started with clear task to reset the task after which it removes the
-     * task itself.
-     */
-    public static void resetPendingActivityResults(Launcher launcher, int requestCode) {
-        launcher.onActivityResult(requestCode, RESULT_CANCELED, null);
-        launcher.startActivity(ProxyActivityStarter.getLaunchIntent(launcher, null));
-    }
-
-    public static ScaleAndTranslation getOverviewScaleAndTranslationForNormalState(Launcher l) {
-        if (SysUINavigationMode.getMode(l) == Mode.NO_BUTTON) {
-            float offscreenTranslationX = l.getDeviceProfile().widthPx
-                    - l.getOverviewPanel().getPaddingStart();
-            return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
-        }
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
-    public static Person[] getPersons(ShortcutInfo si) {
-        Person[] persons = si.getPersons();
-        return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons;
-    }
-
-    /** Closes system windows. */
-    public static void closeSystemWindows() {
-        ActivityManagerWrapper.getInstance()
-            .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
index 711e59a..36c0e34 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
@@ -27,6 +27,7 @@
 import android.os.Handler;
 import android.os.Looper;
 
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.system.TonalCompat;
 import com.android.systemui.shared.system.TonalCompat.ExtractionInfo;
 
@@ -39,17 +40,8 @@
     private static final int MAIN_COLOR_DARK = 0xff202124;
     private static final int MAIN_COLOR_REGULAR = 0xff000000;
 
-    private static final Object sInstanceLock = new Object();
-    private static WallpaperColorInfo sInstance;
-
-    public static WallpaperColorInfo getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                sInstance = new WallpaperColorInfo(context.getApplicationContext());
-            }
-            return sInstance;
-        }
-    }
+    public static final MainThreadInitializedObject<WallpaperColorInfo> INSTANCE =
+            new MainThreadInitializedObject<>(WallpaperColorInfo::new);
 
     private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
     private final WallpaperManager mWallpaperManager;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
index 6e7c087..2e422b7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java
@@ -14,8 +14,12 @@
 
 package com.android.launcher3.uioverrides.plugins;
 
+import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.plugins.Plugin;
@@ -24,6 +28,9 @@
 import com.android.systemui.shared.plugins.PluginManagerImpl;
 import com.android.systemui.shared.plugins.PluginPrefs;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 public class PluginManagerWrapper {
@@ -75,4 +82,27 @@
     public static boolean hasPlugins(Context context) {
         return PluginPrefs.hasPlugins(context);
     }
+
+    public void dump(PrintWriter pw) {
+        final List<ComponentName> enabledPlugins = new ArrayList<>();
+        final List<ComponentName> disabledPlugins = new ArrayList<>();
+        for (String action : getPluginActions()) {
+            for (ResolveInfo resolveInfo : mContext.getPackageManager().queryIntentServices(
+                    new Intent(action), MATCH_DISABLED_COMPONENTS)) {
+                ComponentName installedPlugin = new ComponentName(
+                        resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
+                if (mPluginEnabler.isEnabled(installedPlugin)) {
+                    enabledPlugins.add(installedPlugin);
+                } else {
+                    disabledPlugins.add(installedPlugin);
+                }
+            }
+        }
+
+        pw.println("PluginManager:");
+        pw.println("  numEnabledPlugins=" + enabledPlugins.size());
+        pw.println("  numDisabledPlugins=" + disabledPlugins.size());
+        pw.println("  enabledPlugins=" + enabledPlugins);
+        pw.println("  disabledPlugins=" + disabledPlugins);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 85a9545..e7cd393 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -15,10 +15,12 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 
-import com.android.launcher3.AbstractFloatingView;
+import android.content.Context;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -30,7 +32,7 @@
  */
 public class AllAppsState extends LauncherState {
 
-    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY;
+    private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE | FLAG_CLOSE_POPUPS;
 
     private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) {
         @Override
@@ -40,13 +42,12 @@
     };
 
     public AllAppsState(int id) {
-        super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, STATE_FLAGS);
+        super(id, ContainerType.ALLAPPS, STATE_FLAGS);
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher);
-        dispatchWindowStateChanged(launcher);
+    public int getTransitionDuration(Context context) {
+        return 320;
     }
 
     @Override
@@ -64,7 +65,7 @@
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
         ScaleAndTranslation scaleAndTranslation = LauncherState.OVERVIEW
                 .getWorkspaceScaleAndTranslation(launcher);
-        if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
+        if (SysUINavigationMode.getMode(launcher) == NO_BUTTON && !ENABLE_OVERVIEW_ACTIONS.get()) {
             float normalScale = 1;
             // Scale down halfway to where we'd be in overview, to prepare for a potential pause.
             scaleAndTranslation.scale = (scaleAndTranslation.scale + normalScale) / 2;
@@ -75,6 +76,11 @@
     }
 
     @Override
+    protected float getDepthUnchecked(Context context) {
+        return 1f;
+    }
+
+    @Override
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
         return PAGE_ALPHA_PROVIDER;
     }
@@ -85,9 +91,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        float slightParallax = -launcher.getDeviceProfile().allAppsCellHeightPx * 0.3f;
-        return new ScaleAndTranslation(0.9f, 0f, slightParallax);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {0.9f, 0};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
index 39b0f8d..bef191e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
@@ -9,12 +9,12 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationComponents;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
 
 /**
  * Touch controller for handling edge swipes in landscape/seascape UI
@@ -56,7 +56,7 @@
     }
 
     @Override
-    protected float initCurrentAnimation(@AnimationComponents int animComponent) {
+    protected float initCurrentAnimation(@AnimationFlags int animComponent) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
         mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
@@ -73,7 +73,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            RecentsModel.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index db6a40f..20ee61d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -16,41 +16,45 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 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.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.OverviewInteractionState;
-import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * Touch controller for handling various state transitions in portrait UI.
@@ -88,8 +92,7 @@
     protected boolean canInterceptTouch(MotionEvent ev) {
         if (mCurrentAnimation != null) {
             if (mFinishFastOnSecondTouch) {
-                // TODO: Animate to finish instead.
-                mCurrentAnimation.skipToEnd();
+                mCurrentAnimation.getAnimationPlayer().end();
             }
 
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
@@ -122,7 +125,7 @@
                 return false;
             }
         }
-        if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE) != null) {
+        if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
             return false;
         }
         return true;
@@ -130,15 +133,34 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState");
+        }
         if (fromState == ALL_APPS && !isDragTowardPositive) {
             // Should swipe down go to OVERVIEW instead?
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
+                        "PortraitStatesTouchController.getTargetState 1");
+            }
             return TouchInteractionService.isConnected() ?
                     mLauncher.getStateManager().getLastState() : NORMAL;
         } else if (fromState == OVERVIEW) {
-            return isDragTowardPositive ? ALL_APPS : NORMAL;
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
+                        "PortraitStatesTouchController.getTargetState 2");
+            }
+            LauncherState positiveDragTarget = ALL_APPS;
+            if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) {
+                // Don't allow swiping up to all apps.
+                positiveDragTarget = OVERVIEW;
+            }
+            return isDragTowardPositive ? positiveDragTarget : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
-            int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher)
-                    .getSystemUiStateFlags();
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
+                        "PortraitStatesTouchController.getTargetState 3");
+            }
+            int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
             return mAllowDragToOverview && TouchInteractionService.isConnected()
                     && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0
                     ? OVERVIEW : ALL_APPS;
@@ -151,16 +173,16 @@
         return isTouchOverHotseat(mLauncher, ev) ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
     }
 
-    private AnimatorSetBuilder getNormalToOverviewAnimation() {
+    private StateAnimationConfig getNormalToOverviewAnimation() {
         mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR;
 
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+        StateAnimationConfig builder = new StateAnimationConfig();
         builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper);
         return builder;
     }
 
-    public static AnimatorSetBuilder getOverviewToAllAppsAnimation() {
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    private static StateAnimationConfig getOverviewToAllAppsAnimation() {
+        StateAnimationConfig builder = new StateAnimationConfig();
         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
                 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
         builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL,
@@ -168,8 +190,8 @@
         return builder;
     }
 
-    private AnimatorSetBuilder getAllAppsToOverviewAnimation() {
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    private StateAnimationConfig getAllAppsToOverviewAnimation() {
+        StateAnimationConfig builder = new StateAnimationConfig();
         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
                 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
         builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL,
@@ -177,40 +199,42 @@
         return builder;
     }
 
-    private AnimatorSetBuilder getNormalToAllAppsAnimation() {
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    private StateAnimationConfig getNormalToAllAppsAnimation() {
+        StateAnimationConfig builder = new StateAnimationConfig();
         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
                 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
         return builder;
     }
 
-    private AnimatorSetBuilder getAllAppsToNormalAnimation() {
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    private StateAnimationConfig getAllAppsToNormalAnimation() {
+        StateAnimationConfig builder = new StateAnimationConfig();
         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
                 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
         return builder;
     }
 
     @Override
-    protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState,
-            LauncherState toState) {
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+    protected StateAnimationConfig getConfigForStates(
+            LauncherState fromState, LauncherState toState) {
+        final StateAnimationConfig config;
         if (fromState == NORMAL && toState == OVERVIEW) {
-            builder = getNormalToOverviewAnimation();
+            config = getNormalToOverviewAnimation();
         } else if (fromState == OVERVIEW && toState == ALL_APPS) {
-            builder = getOverviewToAllAppsAnimation();
+            config = getOverviewToAllAppsAnimation();
         } else if (fromState == ALL_APPS && toState == OVERVIEW) {
-            builder = getAllAppsToOverviewAnimation();
+            config = getAllAppsToOverviewAnimation();
         } else if (fromState == NORMAL && toState == ALL_APPS) {
-            builder = getNormalToAllAppsAnimation();
+            config = getNormalToAllAppsAnimation();
         } else if (fromState == ALL_APPS && toState == NORMAL) {
-            builder = getAllAppsToNormalAnimation();
+            config = getAllAppsToNormalAnimation();
+        }  else {
+            config = new StateAnimationConfig();
         }
-        return builder;
+        return config;
     }
 
     @Override
-    protected float initCurrentAnimation(@AnimationComponents int animComponents) {
+    protected float initCurrentAnimation(@AnimationFlags int animFlags) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
 
@@ -219,9 +243,10 @@
 
         float totalShift = endVerticalShift - startVerticalShift;
 
-        final AnimatorSetBuilder builder = totalShift == 0 ? new AnimatorSetBuilder()
-                : getAnimatorSetBuilderForStates(mFromState, mToState);
-        updateAnimatorBuilderOnReinit(builder);
+        final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig()
+                : getConfigForStates(mFromState, mToState);
+        config.animFlags = updateAnimComponentsOnReinit(animFlags);
+        config.duration = maxAccuracy;
 
         cancelPendingAnim();
 
@@ -230,22 +255,21 @@
             // Reset the state manager, when changing the interaction mode
             mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */);
             mPendingAnimation = mOverviewPortraitStateTouchHelper
-                    .createSwipeDownToTaskAppAnimation(maxAccuracy);
-            mPendingAnimation.anim.setInterpolator(Interpolators.LINEAR);
-
+                    .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR);
             Runnable onCancelRunnable = () -> {
                 cancelPendingAnim();
                 clearState();
             };
-            mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy,
-                    onCancelRunnable);
+            mCurrentAnimation = mPendingAnimation.createPlaybackController()
+                    .setOnCancelRunnable(onCancelRunnable);
             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
+            RecentsView recentsView = mLauncher.getOverviewPanel();
             totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
-                    mLauncher.getDeviceProfile());
+                    mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
         } else {
             mCurrentAnimation = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState,
-                            animComponents);
+                    .createAnimationToNewWorkspace(mToState, config)
+                    .setOnCancelRunnable(this::clearState);
         }
 
         if (totalShift == 0) {
@@ -258,7 +282,9 @@
     /**
      * Give subclasses the chance to update the animation when we re-initialize towards a new state.
      */
-    protected void updateAnimatorBuilderOnReinit(AnimatorSetBuilder builder) {
+    @AnimationFlags
+    protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) {
+        return animComponents;
     }
 
     private void cancelPendingAnim() {
@@ -279,7 +305,7 @@
     private void handleFirstSwipeToOverview(final ValueAnimator animator,
             final long expectedDuration, final LauncherState targetState, final float velocity,
             final boolean isFling) {
-        if (QUICKSTEP_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
+        if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
                 && targetState == OVERVIEW) {
             mFinishFastOnSecondTouch = true;
         } else  if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
@@ -301,7 +327,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            RecentsModel.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index 11a8043..16bd9ed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -21,8 +21,6 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 
 import android.graphics.PointF;
-import android.os.RemoteException;
-import android.util.Log;
 import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -37,9 +35,8 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TouchController;
-import com.android.quickstep.RecentsModel;
-import com.android.systemui.shared.recents.ISystemUiProxy;
 
+import com.android.quickstep.SystemUiProxy;
 import java.io.PrintWriter;
 
 /**
@@ -62,9 +59,9 @@
      */
     private static final int FLAG_SLIPPERY = 0x20000000;
 
-    protected final Launcher mLauncher;
+    private final Launcher mLauncher;
+    private final SystemUiProxy mSystemUiProxy;
     private final float mTouchSlop;
-    private ISystemUiProxy mSysUiProxy;
     private int mLastAction;
     private final SparseArray<PointF> mDownEvents;
 
@@ -73,6 +70,7 @@
 
     public StatusBarTouchController(Launcher l) {
         mLauncher = l;
+        mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher);
         // Guard against TAPs by increasing the touch slop.
         mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop();
         mDownEvents = new SparseArray<>();
@@ -82,17 +80,14 @@
     public void dump(String prefix, PrintWriter writer) {
         writer.println(prefix + "mCanIntercept:" + mCanIntercept);
         writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction));
-        writer.println(prefix + "mSysUiProxy available:" + (mSysUiProxy != null));
+        writer.println(prefix + "mSysUiProxy available:"
+                + SystemUiProxy.INSTANCE.get(mLauncher).isActive());
     }
 
     private void dispatchTouchEvent(MotionEvent ev) {
-        try {
-            if (mSysUiProxy != null) {
-                mLastAction = ev.getActionMasked();
-                mSysUiProxy.onStatusBarMotionEvent(ev);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Remote exception on sysUiProxy.", e);
+        if (mSystemUiProxy.isActive()) {
+            mLastAction = ev.getActionMasked();
+            mSystemUiProxy.onStatusBarMotionEvent(ev);
         }
     }
 
@@ -170,7 +165,6 @@
                 return false;
             }
         }
-        mSysUiProxy = RecentsModel.INSTANCE.get(mLauncher).getSystemUiProxy();
-        return mSysUiProxy != null;
+        return SystemUiProxy.INSTANCE.get(mLauncher).isActive();
     }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
deleted file mode 100644
index 110cc23..0000000
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.os.Build;
-import android.os.Handler;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.BiPredicate;
-import java.util.function.Consumer;
-
-/**
- * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public interface ActivityControlHelper<T extends BaseDraggingActivity> {
-
-    void onTransitionCancelled(T activity, boolean activityVisible);
-
-    int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect);
-
-    void onSwipeUpToRecentsComplete(T activity);
-
-    default void onSwipeUpToHomeComplete(T activity) { }
-    void onAssistantVisibilityChanged(float visibility);
-
-    @NonNull HomeAnimationFactory prepareHomeUI(T activity);
-
-    AnimationFactory prepareRecentsUI(T activity, boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback);
-
-    ActivityInitListener createActivityInitListener(BiPredicate<T, Boolean> onInitListener);
-
-    @Nullable
-    T getCreatedActivity();
-
-    default boolean isResumed() {
-        BaseDraggingActivity activity = getCreatedActivity();
-        return activity != null && activity.hasBeenResumed();
-    }
-
-    @UiThread
-    @Nullable
-    <T extends View> T getVisibleRecentsView();
-
-    @UiThread
-    boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
-
-    Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target);
-
-    boolean shouldMinimizeSplitScreen();
-
-    default boolean deferStartingActivity(Region activeNavBarRegion, MotionEvent ev) {
-        return true;
-    }
-
-    /**
-     * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
-     */
-    int getContainerType();
-
-    boolean isInLiveTileMode();
-
-    void onLaunchTaskFailed(T activity);
-
-    void onLaunchTaskSuccess(T activity);
-
-    interface ActivityInitListener {
-
-        void register();
-
-        void unregister();
-
-        void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
-                Context context, Handler handler, long duration);
-    }
-
-    interface AnimationFactory {
-
-        default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { }
-
-        void createActivityController(long transitionLength);
-
-        default void adjustActivityControllerInterpolators() { }
-
-        default void onTransitionCancelled() { }
-
-        default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
-                Interpolator interpolator, long duration) { }
-
-        /**
-         * @param attached Whether to show RecentsView alongside the app window. If false, recents
-         *                 will be hidden by some property we can animate, e.g. alpha.
-         * @param animate Whether to animate recents to/from its new attached state.
-         */
-        default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
-    }
-
-    interface HomeAnimationFactory {
-
-        /** Return the floating view that will animate in sync with the closing window. */
-        default @Nullable View getFloatingView() {
-            return null;
-        }
-
-        @NonNull RectF getWindowTargetRect();
-
-        @NonNull AnimatorPlaybackController createActivityAnimationToHome();
-
-        default void playAtomicAnimation(float velocity) {
-            // No-op
-        }
-
-        static RectF getDefaultWindowTargetRect(DeviceProfile dp) {
-            final int halfIconSize = dp.iconSizePx / 2;
-            final float targetCenterX = dp.availableWidthPx / 2f;
-            final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
-            // Fallback to animate to center of screen.
-            return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
-                    targetCenterX + halfIconSize, targetCenterY + halfIconSize);
-        }
-
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index 84dfdbd..f7e8781 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -25,23 +25,30 @@
  */
 public class AnimatedFloat {
 
-    public static FloatProperty<AnimatedFloat> VALUE = new FloatProperty<AnimatedFloat>("value") {
-        @Override
-        public void setValue(AnimatedFloat obj, float v) {
-            obj.updateValue(v);
-        }
+    public static final FloatProperty<AnimatedFloat> VALUE =
+            new FloatProperty<AnimatedFloat>("value") {
+                @Override
+                public void setValue(AnimatedFloat obj, float v) {
+                    obj.updateValue(v);
+                }
 
-        @Override
-        public Float get(AnimatedFloat obj) {
-            return obj.value;
-        }
-    };
+                @Override
+                public Float get(AnimatedFloat obj) {
+                    return obj.value;
+                }
+            };
+
+    private static final Runnable NO_OP = () -> { };
 
     private final Runnable mUpdateCallback;
     private ObjectAnimator mValueAnimator;
 
     public float value;
 
+    public AnimatedFloat() {
+        this(NO_OP);
+    }
+
     public AnimatedFloat(Runnable updateCallback) {
         mUpdateCallback = updateCallback;
     }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
new file mode 100644
index 0000000..2b698bd
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -0,0 +1,412 @@
+/*
+ * 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.quickstep;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.SysUINavigationMode.getMode;
+import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+
+import android.animation.Animator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
+        ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
+
+    public final boolean rotationSupportedByActivity;
+
+    private final STATE_TYPE mOverviewState, mBackgroundState;
+
+    protected BaseActivityInterface(boolean rotationSupportedByActivity,
+            STATE_TYPE overviewState, STATE_TYPE backgroundState) {
+        this.rotationSupportedByActivity = rotationSupportedByActivity;
+        mOverviewState = overviewState;
+        mBackgroundState = backgroundState;
+    }
+
+    public void onTransitionCancelled(boolean activityVisible) {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        STATE_TYPE startState = activity.getStateManager().getRestState();
+        activity.getStateManager().goToState(startState, activityVisible);
+    }
+
+    public abstract int getSwipeUpDestinationAndLength(
+            DeviceProfile dp, Context context, Rect outRect,
+            PagedOrientationHandler orientationHandler);
+
+    public void onSwipeUpToRecentsComplete() {
+        // Re apply state in case we did something funky during the transition.
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.getStateManager().reapplyState();
+    }
+
+    public abstract void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState);
+
+    public abstract void onAssistantVisibilityChanged(float visibility);
+
+    public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+            boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
+
+    public abstract ActivityInitListener createActivityInitListener(
+            Predicate<Boolean> onInitListener);
+
+    /**
+     * Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
+     */
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {}
+
+    @Nullable
+    public abstract ACTIVITY_TYPE getCreatedActivity();
+
+    @Nullable
+    public DepthController getDepthController() {
+        return null;
+    }
+
+    public final boolean isResumed() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        return activity != null && activity.hasBeenResumed();
+    }
+
+    public final boolean isStarted() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        return activity != null && activity.isStarted();
+    }
+
+    @UiThread
+    @Nullable
+    public abstract <T extends RecentsView> T getVisibleRecentsView();
+
+    @UiThread
+    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+
+    public abstract Rect getOverviewWindowBounds(
+            Rect homeBounds, RemoteAnimationTargetCompat target);
+
+    public abstract boolean allowMinimizeSplitScreen();
+
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        return deviceState.isInDeferredGestureRegion(ev);
+    }
+
+    public abstract void onExitOverview(RecentsAnimationDeviceState deviceState,
+            Runnable exitRunnable);
+
+    /**
+     * Updates the prediction state to the overview state.
+     */
+    public void updateOverviewPredictionState() {
+        // By public overview predictions are not supported
+    }
+
+    /**
+     * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
+     */
+    public abstract int getContainerType();
+
+    public abstract boolean isInLiveTileMode();
+
+    public abstract void onLaunchTaskFailed();
+
+    public void onLaunchTaskSuccess() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.getStateManager().moveToRestState();
+    }
+
+    public void closeOverlay() { }
+
+    public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        RecentsView recentsView = activity.getOverviewPanel();
+        if (recentsView == null) {
+            if (runnable != null) {
+                runnable.run();
+            }
+            return;
+        }
+        recentsView.switchToScreenshot(thumbnailData, runnable);
+    }
+
+    /**
+     * Calculates the taskView size for the provided device configuration
+     */
+    public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
+            PagedOrientationHandler orientedState) {
+        calculateTaskSize(context, dp, getExtraSpace(context, dp, orientedState),
+                outRect, orientedState);
+    }
+
+    protected abstract float getExtraSpace(Context context, DeviceProfile dp,
+            PagedOrientationHandler orientedState);
+
+    private void calculateTaskSize(
+            Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect,
+            PagedOrientationHandler orientationHandler) {
+        Resources res = context.getResources();
+        final boolean showLargeTaskSize = showOverviewActions(context) ||
+                hideShelfInTwoButtonLandscape(context, orientationHandler);
+
+        final int paddingResId;
+        if (dp.isMultiWindowMode) {
+            paddingResId = R.dimen.multi_window_task_card_horz_space;
+        } else if (dp.isVerticalBarLayout()) {
+            paddingResId = R.dimen.landscape_task_card_horz_space;
+        } else if (showLargeTaskSize) {
+            paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
+        } else {
+            paddingResId = R.dimen.portrait_task_card_horz_space;
+        }
+        float paddingHorz = res.getDimension(paddingResId);
+        float paddingVert = showLargeTaskSize
+                ? 0 : res.getDimension(R.dimen.task_card_vert_space);
+
+        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
+                res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
+    }
+
+    private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
+            float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
+            Rect outRect) {
+        float taskWidth, taskHeight;
+        Rect insets = dp.getInsets();
+        if (dp.isMultiWindowMode) {
+            WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
+            taskWidth = bounds.availableSize.x;
+            taskHeight = bounds.availableSize.y;
+        } else {
+            taskWidth = dp.availableWidthPx;
+            taskHeight = dp.availableHeightPx;
+        }
+
+        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
+        // we override the insets ourselves.
+        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
+        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
+
+        float availableHeight = launcherVisibleHeight
+                - topIconMargin - extraVerticalSpace - paddingVert;
+        float availableWidth = launcherVisibleWidth - paddingHorz;
+
+        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
+        float outWidth = scale * taskWidth;
+        float outHeight = scale * taskHeight;
+
+        // Center in the visible space
+        float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
+        float y = insets.top + Math.max(topIconMargin,
+                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
+        outRect.set(Math.round(x), Math.round(y),
+                Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
+    }
+
+    /**
+     * Calculates the modal taskView size for the provided device configuration
+     */
+    public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
+        float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
+                ? R.dimen.multi_window_task_card_horz_space
+                : dp.isVerticalBarLayout()
+                        ? R.dimen.landscape_task_card_horz_space
+                        : R.dimen.portrait_modal_task_card_horz_space);
+        float extraVerticalSpace = getOverviewActionsHeight(context);
+        float paddingVert = 0;
+        float topIconMargin = 0;
+        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
+                topIconMargin, outRect);
+    }
+
+    /** Gets the space that the overview actions will take, including margins. */
+    public final float getOverviewActionsHeight(Context context) {
+        Resources res = context.getResources();
+        float actionsBottomMargin = 0;
+        if (getMode(context) == Mode.THREE_BUTTONS) {
+            actionsBottomMargin = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_bottom_margin_three_button);
+        } else {
+            actionsBottomMargin = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_bottom_margin_gesture);
+        }
+        float overviewActionsHeight = actionsBottomMargin
+                + res.getDimensionPixelSize(R.dimen.overview_actions_height);
+        return overviewActionsHeight;
+    }
+
+    public interface AnimationFactory {
+
+        void createActivityInterface(long transitionLength);
+
+        default void onTransitionCancelled() { }
+
+        default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
+                Interpolator interpolator, long duration) { }
+
+        /**
+         * @param attached Whether to show RecentsView alongside the app window. If false, recents
+         *                 will be hidden by some property we can animate, e.g. alpha.
+         * @param animate Whether to animate recents to/from its new attached state.
+         */
+        default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
+    }
+
+    class DefaultAnimationFactory implements AnimationFactory {
+
+        protected final ACTIVITY_TYPE mActivity;
+        private final STATE_TYPE mStartState;
+        private final Consumer<AnimatorPlaybackController> mCallback;
+
+        private boolean mIsAttachedToWindow;
+
+        DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
+            mCallback = callback;
+
+            mActivity = getCreatedActivity();
+            mStartState = mActivity.getStateManager().getState();
+        }
+
+        protected ACTIVITY_TYPE initUI() {
+            STATE_TYPE resetState = mStartState;
+            if (mStartState.shouldDisableRestore()) {
+                resetState = mActivity.getStateManager().getRestState();
+            }
+            mActivity.getStateManager().setRestState(resetState);
+            mActivity.getStateManager().goToState(mBackgroundState, false);
+            return mActivity;
+        }
+
+        @Override
+        public void createActivityInterface(long transitionLength) {
+            PendingAnimation pa = new PendingAnimation(transitionLength * 2);
+            createBackgroundToOverviewAnim(mActivity, pa);
+            AnimatorPlaybackController controller = pa.createPlaybackController();
+            mActivity.getStateManager().setCurrentUserControlledAnimation(controller);
+
+            // Since we are changing the start position of the UI, reapply the state, at the end
+            controller.setEndAction(() -> mActivity.getStateManager().goToState(
+                    controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
+                    false));
+            mCallback.accept(controller);
+
+            // Creating the activity controller animation sometimes reapplies the launcher state
+            // (because we set the animation as the current state animation), so we reapply the
+            // attached state here as well to ensure recents is shown/hidden appropriately.
+            if (SysUINavigationMode.getMode(mActivity) == Mode.NO_BUTTON) {
+                setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
+            }
+        }
+
+        @Override
+        public void onTransitionCancelled() {
+            mActivity.getStateManager().goToState(mStartState, false /* animate */);
+        }
+
+        @Override
+        public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+            if (mIsAttachedToWindow == attached && animate) {
+                return;
+            }
+            mIsAttachedToWindow = attached;
+            RecentsView recentsView = mActivity.getOverviewPanel();
+            Animator fadeAnim = mActivity.getStateManager()
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+
+            float fromTranslation = attached ? 1 : 0;
+            float toTranslation = attached ? 0 : 1;
+            mActivity.getStateManager()
+                    .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+            if (!recentsView.isShown() && animate) {
+                ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
+            } else {
+                fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
+            }
+            if (!animate) {
+                ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
+            } else {
+                mActivity.getStateManager().createStateElementAnimation(
+                        INDEX_RECENTS_TRANSLATE_X_ANIM,
+                        fromTranslation, toTranslation).start();
+            }
+
+            fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+            fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+        }
+
+        protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
+            //  Scale down recents from being full screen to being in overview.
+            RecentsView recentsView = activity.getOverviewPanel();
+            pa.addFloat(recentsView, SCALE_PROPERTY,
+                    recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
+            pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
+        }
+    }
+
+    protected static boolean showOverviewActions(Context context) {
+        return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
deleted file mode 100644
index 1ac7ed4..0000000
--- a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
-import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
-
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Bundle;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.uioverrides.UiFactory;
-import com.android.launcher3.util.SystemUiController;
-import com.android.launcher3.util.Themes;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
-/**
- * A base fallback recents activity that provides support for device profile changes, activity
- * lifecycle tracking, and basic input handling from recents.
- *
- * This class is only used as a fallback in case the default launcher does not have a recents
- * implementation.
- */
-public abstract class BaseRecentsActivity extends BaseDraggingActivity {
-
-    private Configuration mOldConfig;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        mOldConfig = new Configuration(getResources().getConfiguration());
-        initDeviceProfile();
-        initViews();
-
-        getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
-                Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
-        RecentsActivityTracker.onRecentsActivityCreate(this);
-    }
-
-    /**
-     * Init drag layer and overview panel views.
-     */
-    abstract protected void initViews();
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        int diff = newConfig.diff(mOldConfig);
-        if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
-            onHandleConfigChanged();
-        }
-        mOldConfig.setTo(newConfig);
-        super.onConfigurationChanged(newConfig);
-    }
-
-    /**
-     * Logic for when device configuration changes (rotation, screen size change, multi-window,
-     * etc.)
-     */
-    protected void onHandleConfigChanged() {
-        mUserEventDispatcher = null;
-        initDeviceProfile();
-
-        AbstractFloatingView.closeOpenViews(this, true,
-                AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
-        dispatchDeviceProfileChanged();
-
-        reapplyUi();
-    }
-
-    /**
-     * Initialize/update the device profile.
-     */
-    private void initDeviceProfile() {
-        mDeviceProfile = createDeviceProfile();
-        onDeviceProfileInitiated();
-    }
-
-    /**
-     * Generate the device profile to use in this activity.
-     * @return device profile
-     */
-    protected DeviceProfile createDeviceProfile() {
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
-
-        // In case we are reusing IDP, create a copy so that we don't conflict with Launcher
-        // activity.
-        return dp.copy(this);
-    }
-
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-
-        // Workaround for b/78520668, explicitly trim memory once UI is hidden
-        onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
-    }
-
-    @Override
-    public void onEnterAnimationComplete() {
-        super.onEnterAnimationComplete();
-        UiFactory.onEnterAnimationComplete(this);
-    }
-
-    @Override
-    public void onTrimMemory(int level) {
-        super.onTrimMemory(level);
-        UiFactory.onTrimMemory(this, level);
-    }
-
-    @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        RecentsActivityTracker.onRecentsActivityNewIntent(this);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        RecentsActivityTracker.onRecentsActivityDestroy(this);
-    }
-
-    @Override
-    public void onBackPressed() {
-        // TODO: Launch the task we came from
-        startHome();
-    }
-
-    public void startHome() {
-        startActivity(new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-    }
-
-    @Override
-    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
-        super.dump(prefix, fd, writer, args);
-        writer.println(prefix + "Misc:");
-        dumpMisc(prefix + "\t", writer);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
new file mode 100644
index 0000000..00b5eb9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Manages the state for an active system gesture, listens for events from the system and Launcher,
+ * and fires events when the states change.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationListener {
+
+    /**
+     * Defines the end targets of a gesture and the associated state.
+     */
+    public enum GestureEndTarget {
+        HOME(true, ContainerType.WORKSPACE, false),
+
+        RECENTS(true, ContainerType.TASKSWITCHER, true),
+
+        NEW_TASK(false, ContainerType.APP, true),
+
+        LAST_TASK(false, ContainerType.APP, true);
+
+        GestureEndTarget(boolean isLauncher, int containerType,
+                boolean recentsAttachedToAppWindow) {
+            this.isLauncher = isLauncher;
+            this.containerType = containerType;
+            this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
+        }
+
+        /** Whether the target is in the launcher activity. Implicitly, if the end target is going
+         to Launcher, then we can not interrupt the animation to start another gesture. */
+        public final boolean isLauncher;
+        /** Used to log where the user ended up after the gesture ends */
+        public final int containerType;
+        /** Whether RecentsView should be attached to the window as we animate to this target */
+        public final boolean recentsAttachedToAppWindow;
+    }
+
+    private static final String TAG = "GestureState";
+
+    private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
+    public static final GestureState DEFAULT_STATE = new GestureState();
+
+    private static int FLAG_COUNT = 0;
+    private static int getFlagForIndex(String name) {
+        if (DEBUG_STATES) {
+            STATE_NAMES.add(name);
+        }
+        int index = 1 << FLAG_COUNT;
+        FLAG_COUNT++;
+        return index;
+    }
+
+    // Called when the end target as been set
+    public static final int STATE_END_TARGET_SET =
+            getFlagForIndex("STATE_END_TARGET_SET");
+
+    // Called when the end target animation has finished
+    public static final int STATE_END_TARGET_ANIMATION_FINISHED =
+            getFlagForIndex("STATE_END_TARGET_ANIMATION_FINISHED");
+
+    // Called when the recents animation has been requested to start
+    public static final int STATE_RECENTS_ANIMATION_INITIALIZED =
+            getFlagForIndex("STATE_RECENTS_ANIMATION_INITIALIZED");
+
+    // Called when the recents animation is started and the TaskAnimationManager has been updated
+    // with the controller and targets
+    public static final int STATE_RECENTS_ANIMATION_STARTED =
+            getFlagForIndex("STATE_RECENTS_ANIMATION_STARTED");
+
+    // Called when the recents animation is canceled
+    public static final int STATE_RECENTS_ANIMATION_CANCELED =
+            getFlagForIndex("STATE_RECENTS_ANIMATION_CANCELED");
+
+    // Called when the recents animation finishes
+    public static final int STATE_RECENTS_ANIMATION_FINISHED =
+            getFlagForIndex("STATE_RECENTS_ANIMATION_FINISHED");
+
+    // Always called when the recents animation ends (regardless of cancel or finish)
+    public static final int STATE_RECENTS_ANIMATION_ENDED =
+            getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED");
+
+    // Called when we create an overscroll window when swiping right to left on the most recent app
+    public static final int STATE_OVERSCROLL_WINDOW_CREATED =
+            getFlagForIndex("STATE_OVERSCROLL_WINDOW_CREATED");
+
+    // Called when RecentsView stops scrolling and settles on a TaskView.
+    public static final int STATE_RECENTS_SCROLLING_FINISHED =
+            getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED");
+
+    // Needed to interact with the current activity
+    private final Intent mHomeIntent;
+    private final Intent mOverviewIntent;
+    private final BaseActivityInterface mActivityInterface;
+    private final MultiStateCallback mStateCallback;
+    private final int mGestureId;
+
+    private ActivityManager.RunningTaskInfo mRunningTask;
+    private GestureEndTarget mEndTarget;
+    private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
+    private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
+    private int mLastStartedTaskId = -1;
+
+    public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
+        mHomeIntent = componentObserver.getHomeIntent();
+        mOverviewIntent = componentObserver.getOverviewIntent();
+        mActivityInterface = componentObserver.getActivityInterface();
+        mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
+        mGestureId = gestureId;
+    }
+
+    public GestureState(GestureState other) {
+        mHomeIntent = other.mHomeIntent;
+        mOverviewIntent = other.mOverviewIntent;
+        mActivityInterface = other.mActivityInterface;
+        mStateCallback = other.mStateCallback;
+        mGestureId = other.mGestureId;
+        mRunningTask = other.mRunningTask;
+        mEndTarget = other.mEndTarget;
+        mLastAppearedTaskTarget = other.mLastAppearedTaskTarget;
+        mPreviouslyAppearedTaskIds = other.mPreviouslyAppearedTaskIds;
+        mLastStartedTaskId = other.mLastStartedTaskId;
+    }
+
+    public GestureState() {
+        // Do nothing, only used for initializing the gesture state prior to user unlock
+        mHomeIntent = new Intent();
+        mOverviewIntent = new Intent();
+        mActivityInterface = null;
+        mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
+        mGestureId = -1;
+    }
+
+    /**
+     * @return whether the gesture state has the provided {@param stateMask} flags set.
+     */
+    public boolean hasState(int stateMask) {
+        return mStateCallback.hasStates(stateMask);
+    }
+
+    /**
+     * Sets the given {@param stateFlag}s.
+     */
+    public void setState(int stateFlag) {
+        mStateCallback.setState(stateFlag);
+    }
+
+    /**
+     * Adds a callback for when the states matching the given {@param stateMask} is set.
+     */
+    public void runOnceAtState(int stateMask, Runnable callback) {
+        mStateCallback.runOnceAtState(stateMask, callback);
+    }
+
+    /**
+     * @return the intent for the Home component.
+     */
+    public Intent getHomeIntent() {
+        return mHomeIntent;
+    }
+
+    /**
+     * @return the intent for the Overview component.
+     */
+    public Intent getOverviewIntent() {
+        return mOverviewIntent;
+    }
+
+    /**
+     * @return the interface to the activity handing the UI updates for this gesture.
+     */
+    public <T extends StatefulActivity<?>> BaseActivityInterface<?, T> getActivityInterface() {
+        return mActivityInterface;
+    }
+
+    /**
+     * @return the id for this particular gesture.
+     */
+    public int getGestureId() {
+        return mGestureId;
+    }
+
+    /**
+     * @return the running task for this gesture.
+     */
+    public ActivityManager.RunningTaskInfo getRunningTask() {
+        return mRunningTask;
+    }
+
+    /**
+     * @return the running task id for this gesture.
+     */
+    public int getRunningTaskId() {
+        return mRunningTask != null ? mRunningTask.taskId : -1;
+    }
+
+    /**
+     * Updates the running task for the gesture to be the given {@param runningTask}.
+     */
+    public void updateRunningTask(ActivityManager.RunningTaskInfo runningTask) {
+        mRunningTask = runningTask;
+    }
+
+    /**
+     * Updates the last task that appeared during this gesture.
+     */
+    public void updateLastAppearedTaskTarget(RemoteAnimationTargetCompat lastAppearedTaskTarget) {
+        mLastAppearedTaskTarget = lastAppearedTaskTarget;
+        if (lastAppearedTaskTarget != null) {
+            mPreviouslyAppearedTaskIds.add(lastAppearedTaskTarget.taskId);
+        }
+    }
+
+    /**
+     * @return The id of the task that appeared during this gesture.
+     */
+    public int getLastAppearedTaskId() {
+        return mLastAppearedTaskTarget != null ? mLastAppearedTaskTarget.taskId : -1;
+    }
+
+    public void updatePreviouslyAppearedTaskIds(Set<Integer> previouslyAppearedTaskIds) {
+        mPreviouslyAppearedTaskIds = previouslyAppearedTaskIds;
+    }
+
+    public Set<Integer> getPreviouslyAppearedTaskIds() {
+        return mPreviouslyAppearedTaskIds;
+    }
+
+    /**
+     * Updates the last task that we started via startActivityFromRecents() during this gesture.
+     */
+    public void updateLastStartedTaskId(int lastStartedTaskId) {
+        mLastStartedTaskId = lastStartedTaskId;
+    }
+
+    /**
+     * @return The id of the task that was most recently started during this gesture, or -1 if
+     * no task has been started yet (i.e. we haven't settled on a new task).
+     */
+    public int getLastStartedTaskId() {
+        return mLastStartedTaskId;
+    }
+
+    /**
+     * @return the end target for this gesture (if known).
+     */
+    public GestureEndTarget getEndTarget() {
+        return mEndTarget;
+    }
+
+    /**
+     * Sets the end target of this gesture and immediately notifies the state changes.
+     */
+    public void setEndTarget(GestureEndTarget target) {
+        setEndTarget(target, true /* isAtomic */);
+    }
+
+    /**
+     * Sets the end target of this gesture, but if {@param isAtomic} is {@code false}, then the
+     * caller must explicitly set {@link #STATE_END_TARGET_ANIMATION_FINISHED} themselves.
+     */
+    public void setEndTarget(GestureEndTarget target, boolean isAtomic) {
+        mEndTarget = target;
+        mStateCallback.setState(STATE_END_TARGET_SET);
+        ActiveGestureLog.INSTANCE.addLog("setEndTarget " + mEndTarget);
+        if (isAtomic) {
+            mStateCallback.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+        }
+    }
+
+    /**
+     * @return whether the current gesture is still running a recents animation to a state in the
+     *         Launcher or Recents activity.
+     */
+    public boolean isRunningAnimationToLauncher() {
+        return isRecentsAnimationRunning() && mEndTarget != null && mEndTarget.isLauncher;
+    }
+
+    /**
+     * @return whether the recents animation is started but not yet ended
+     */
+    public boolean isRecentsAnimationRunning() {
+        return mStateCallback.hasStates(STATE_RECENTS_ANIMATION_INITIALIZED) &&
+                !mStateCallback.hasStates(STATE_RECENTS_ANIMATION_ENDED);
+    }
+
+    @Override
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
+    }
+
+    @Override
+    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+        mStateCallback.setState(STATE_RECENTS_ANIMATION_CANCELED);
+        mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
+    }
+
+    @Override
+    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+        mStateCallback.setState(STATE_RECENTS_ANIMATION_FINISHED);
+        mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("GestureState:");
+        pw.println("  gestureID=" + mGestureId);
+        pw.println("  runningTask=" + mRunningTask);
+        pw.println("  endTarget=" + mEndTarget);
+        pw.println("  lastAppearedTaskTargetId=" + getLastAppearedTaskId());
+        pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
+        pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
similarity index 68%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
rename to quickstep/src/com/android/quickstep/InputConsumer.java
index a1e5d47..ec720d5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.quickstep.inputconsumers;
+package com.android.quickstep;
 
 import android.annotation.TargetApi;
 import android.os.Build;
@@ -33,6 +33,8 @@
     int TYPE_SCREEN_PINNED = 1 << 6;
     int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
     int TYPE_RESET_GESTURE = 1 << 8;
+    int TYPE_OVERSCROLL = 1 << 9;
+    int TYPE_SYSUI_OVERLAY = 1 << 10;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -44,16 +46,14 @@
             "TYPE_SCREEN_PINNED",           // 6
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
+            "TYPE_OVERSCROLL",              // 9
+            "TYPE_SYSUI_OVERLAY"         // 10
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
 
     int getType();
 
-    default boolean useSharedSwipeState() {
-        return false;
-    }
-
     /**
      * Returns true if the user has crossed the threshold for it to be an explicit action.
      */
@@ -62,7 +62,31 @@
     }
 
     /**
+     * Returns true if the lifecycle of this input consumer is detached from the normal gesture
+     * down/up flow. If so, it is the responsibility of the input consumer to call back to
+     * {@link TouchInteractionService#onConsumerInactive(InputConsumer)} after the consumer is
+     * finished.
+     */
+    default boolean isConsumerDetachedFromGesture() {
+        return false;
+    }
+
+    /**
+     * Handle and specific setup necessary based on the orientation of the device
+     */
+    default void notifyOrientationSetup() {}
+
+    /**
+     * Returns the active input consumer is in the hierarchy of this input consumer.
+     */
+    default InputConsumer getActiveConsumerInHierarchy() {
+        return this;
+    }
+
+    /**
      * Called by the event queue when the consumer is about to be switched to a new consumer.
+     * Consumers should update the state accordingly here before the state is passed to the new
+     * consumer.
      */
     default void onConsumerAboutToBeSwitched() { }
 
@@ -79,15 +103,15 @@
     }
 
     default String getName() {
-        String name = "";
+        StringBuilder name = new StringBuilder();
         for (int i = 0; i < NAMES.length; i++) {
             if ((getType() & (1 << i)) != 0) {
                 if (name.length() > 0) {
-                    name += ":";
+                    name.append(":");
                 }
-                name += NAMES[i];
+                name.append(NAMES[i]);
             }
         }
-        return name;
+        return name.toString();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
index 3e9872a..7638541 100644
--- a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
+++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
@@ -21,7 +21,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
-import com.android.launcher3.AppInfo;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.InstantAppResolver;
 
 /**
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 0000000..b3875ae
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,177 @@
+/*
+ * 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.quickstep;
+
+import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.os.Looper;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.launcher3.config.FeatureFlags;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.StringJoiner;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+    private static final String TAG = "MultiStateCallback";
+    public static final boolean DEBUG_STATES = false;
+
+    private final SparseArray<LinkedList<Runnable>> mCallbacks = new SparseArray<>();
+    private final SparseArray<ArrayList<Consumer<Boolean>>> mStateChangeListeners =
+            new SparseArray<>();
+
+    private final String[] mStateNames;
+
+    private int mState = 0;
+
+    public MultiStateCallback(String[] stateNames) {
+        mStateNames = DEBUG_STATES ? stateNames : null;
+    }
+
+    /**
+     * Adds the provided state flags to the global state on the UI thread and executes any callbacks
+     * as a result.
+     */
+    public void setStateOnUiThread(int stateFlag) {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            setState(stateFlag);
+        } else {
+            postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> setState(stateFlag));
+        }
+    }
+
+    /**
+     * Adds the provided state flags to the global state and executes any callbacks as a result.
+     */
+    public void setState(int stateFlag) {
+        if (DEBUG_STATES) {
+            Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
+                    + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
+        }
+
+        final int oldState = mState;
+        mState = mState | stateFlag;
+
+        int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            int state = mCallbacks.keyAt(i);
+
+            if ((mState & state) == state) {
+                LinkedList<Runnable> callbacks = mCallbacks.valueAt(i);
+                while (!callbacks.isEmpty()) {
+                    callbacks.pollFirst().run();
+                }
+            }
+        }
+        notifyStateChangeListeners(oldState);
+    }
+
+    /**
+     * Adds the provided state flags to the global state and executes any change handlers
+     * as a result.
+     */
+    public void clearState(int stateFlag) {
+        if (DEBUG_STATES) {
+            Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
+                    + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
+        }
+
+        int oldState = mState;
+        mState = mState & ~stateFlag;
+        notifyStateChangeListeners(oldState);
+    }
+
+    private void notifyStateChangeListeners(int oldState) {
+        int count = mStateChangeListeners.size();
+        for (int i = 0; i < count; i++) {
+            int state = mStateChangeListeners.keyAt(i);
+            boolean wasOn = (state & oldState) == state;
+            boolean isOn = (state & mState) == state;
+
+            if (wasOn != isOn) {
+                ArrayList<Consumer<Boolean>> listeners = mStateChangeListeners.valueAt(i);
+                for (Consumer<Boolean> listener : listeners) {
+                    listener.accept(isOn);
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets a callback to be run when the provided states in the given {@param stateMask} is
+     * enabled. The callback is only run *once*, and if the states are already set at the time of
+     * this call then the callback will be made immediately.
+     */
+    public void runOnceAtState(int stateMask, Runnable callback) {
+        if ((mState & stateMask) == stateMask) {
+            callback.run();
+        } else {
+            final LinkedList<Runnable> callbacks;
+            if (mCallbacks.indexOfKey(stateMask) >= 0) {
+                callbacks = mCallbacks.get(stateMask);
+                if (FeatureFlags.IS_STUDIO_BUILD && callbacks.contains(callback)) {
+                    throw new IllegalStateException("Existing callback for state found");
+                }
+            } else {
+                callbacks = new LinkedList<>();
+                mCallbacks.put(stateMask, callbacks);
+            }
+            callbacks.add(callback);
+        }
+    }
+
+    /**
+     * Adds a persistent listener to be called states in the given {@param stateMask} are enabled
+     * or disabled.
+     */
+    public void addChangeListener(int stateMask, Consumer<Boolean> listener) {
+        final ArrayList<Consumer<Boolean>> listeners;
+        if (mStateChangeListeners.indexOfKey(stateMask) >= 0) {
+            listeners = mStateChangeListeners.get(stateMask);
+        } else {
+            listeners = new ArrayList<>();
+            mStateChangeListeners.put(stateMask, listeners);
+        }
+        listeners.add(listener);
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public boolean hasStates(int stateMask) {
+        return (mState & stateMask) == stateMask;
+    }
+
+    private String convertToFlagNames(int flags) {
+        StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
+        for (int i = 0; i < mStateNames.length; i++) {
+            if ((flags & (1 << i)) != 0) {
+                joiner.add(mStateNames[i]);
+            }
+        }
+        return joiner.toString();
+    }
+
+}
diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
deleted file mode 100644
index bd6204a..0000000
--- a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.annotation.TargetApi;
-import android.app.ActivityManager.TaskDescription;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.UserHandle;
-import android.util.LruCache;
-import android.util.SparseArray;
-
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.graphics.DrawableFactory;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.systemui.shared.recents.model.IconLoader;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
-
-/**
- * Extension of {@link IconLoader} with icon normalization support
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class NormalizedIconLoader extends IconLoader {
-
-    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
-    private final DrawableFactory mDrawableFactory;
-    private final boolean mDisableColorExtraction;
-
-    public NormalizedIconLoader(Context context, TaskKeyLruCache<Drawable> iconCache,
-            LruCache<ComponentName, ActivityInfo> activityInfoCache,
-            boolean disableColorExtraction) {
-        super(context, iconCache, activityInfoCache);
-        mDrawableFactory = DrawableFactory.INSTANCE.get(context);
-        mDisableColorExtraction = disableColorExtraction;
-    }
-
-    @Override
-    public Drawable getDefaultIcon(int userId) {
-        synchronized (mDefaultIcons) {
-            BitmapInfo info = mDefaultIcons.get(userId);
-            if (info == null) {
-                info = getBitmapInfo(Resources.getSystem()
-                        .getDrawable(android.R.drawable.sym_def_app_icon), userId, 0, false);
-                mDefaultIcons.put(userId, info);
-            }
-
-            return new FastBitmapDrawable(info);
-        }
-    }
-
-    @Override
-    protected Drawable createBadgedDrawable(Drawable drawable, int userId, TaskDescription desc) {
-        return new FastBitmapDrawable(getBitmapInfo(drawable, userId, desc.getPrimaryColor(),
-                false));
-    }
-
-    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
-            int primaryColor, boolean isInstantApp) {
-        try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
-            if (mDisableColorExtraction) {
-                la.disableColorExtraction();
-            }
-            la.setWrapperBackgroundColor(primaryColor);
-
-            // User version code O, so that the icon is always wrapped in an adaptive icon container
-            return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
-                    Build.VERSION_CODES.O, isInstantApp);
-        }
-    }
-
-    @Override
-    protected Drawable getBadgedActivityIcon(ActivityInfo activityInfo, int userId,
-            TaskDescription desc) {
-        BitmapInfo bitmapInfo = getBitmapInfo(
-                activityInfo.loadUnbadgedIcon(mContext.getPackageManager()),
-                userId,
-                desc.getPrimaryColor(),
-                activityInfo.applicationInfo.isInstantApp());
-        return mDrawableFactory.newIcon(mContext, bitmapInfo, activityInfo);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
new file mode 100644
index 0000000..1081548
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.states.RotationHelper.deltaRotation;
+import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DefaultDisplay;
+
+import java.io.PrintWriter;
+
+/**
+ * Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
+ * See {@link OrientationRectF#applyTransform(MotionEvent, boolean)} for transformation of
+ * MotionEvents from one orientation's coordinate space to another's.
+ *
+ * This class only supports single touch/pointer gesture tracking for touches started in a supported
+ * nav bar region.
+ */
+class OrientationTouchTransformer {
+
+    private static final String TAG = "OrientationTouchTransformer";
+    private static final boolean DEBUG = false;
+    private static final int MAX_ORIENTATIONS = 4;
+
+    private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1;
+
+    private final Matrix mTmpMatrix = new Matrix();
+    private final float[] mTmpPoint = new float[2];
+
+    private SparseArray<OrientationRectF> mSwipeTouchRegions = new SparseArray<>(MAX_ORIENTATIONS);
+    private final RectF mAssistantLeftRegion = new RectF();
+    private final RectF mAssistantRightRegion = new RectF();
+    private int mCurrentDisplayRotation;
+    private boolean mEnableMultipleRegions;
+    private Resources mResources;
+    private OrientationRectF mLastRectTouched;
+    /**
+     * The rotation of the last touched nav bar, whether that be through the last region the user
+     * touched down on or valid rotation user turned their device to.
+     * Note this is different than
+     * {@link #mQuickStepStartingRotation} as it always updates its value on every touch whereas
+     * mQuickstepStartingRotation only updates when device rotation matches touch rotation.
+     */
+    private int mActiveTouchRotation;
+    private SysUINavigationMode.Mode mMode;
+    private QuickStepContractInfo mContractInfo;
+
+    /**
+     * Represents if we're currently in a swipe "session" of sorts. If value is
+     * QUICKSTEP_ROTATION_UNINITIALIZED, then user has not tapped on an active nav region.
+     * Otherwise it will be the rotation of the display when the user first interacted with the
+     * active nav bar region.
+     * The "session" ends when {@link #enableMultipleRegions(boolean, DefaultDisplay.Info)} is
+     * called - usually from a timeout or if user starts interacting w/ the foreground app.
+     *
+     * This is different than {@link #mLastRectTouched} as it can get reset by the system whereas
+     * the rect is purely used for tracking touch interactions and usually this "session" will
+     * outlast the touch interaction.
+     */
+    private int mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
+
+    /** For testability */
+    interface QuickStepContractInfo {
+        float getWindowCornerRadius();
+    }
+
+
+    OrientationTouchTransformer(Resources resources, SysUINavigationMode.Mode mode,
+            QuickStepContractInfo contractInfo) {
+        mResources = resources;
+        mMode = mode;
+        mContractInfo = contractInfo;
+    }
+
+    void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info) {
+        if (mMode == newMode) {
+            return;
+        }
+        this.mMode = newMode;
+        // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
+        // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
+        // It tries to cache and reuse swipe regions whenever possible based only on rotation
+        mSwipeTouchRegions.clear();
+        resetSwipeRegions(info);
+    }
+
+    /**
+     * Sets the current nav bar region to listen to events for as determined by
+     * {@param info}. If multiple nav bar regions are enabled, then this region will be added
+     * alongside other regions.
+     * Ok to call multiple times
+     *
+     * @see #enableMultipleRegions(boolean, DefaultDisplay.Info)
+     */
+    void createOrAddTouchRegion(DefaultDisplay.Info info) {
+        mCurrentDisplayRotation = info.rotation;
+        if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
+                && mCurrentDisplayRotation == 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);
+        if (region != null) {
+            return;
+        }
+
+        if (mEnableMultipleRegions) {
+            mSwipeTouchRegions.put(mCurrentDisplayRotation, createRegionForDisplay(info));
+        } else {
+            resetSwipeRegions(info);
+        }
+    }
+
+    /**
+     * Call when we want to start tracking nav bar touch regions in multiple orientations.
+     * ALSO, you BETTER call this with {@param enableMultipleRegions} set to false once you're done.
+     *
+     * @param enableMultipleRegions Set to true to start tracking multiple nav bar regions
+     * @param info The current displayInfo which will be the start of the quickswitch gesture
+     */
+    void enableMultipleRegions(boolean enableMultipleRegions, DefaultDisplay.Info info) {
+        mEnableMultipleRegions = enableMultipleRegions &&
+                mMode != SysUINavigationMode.Mode.TWO_BUTTONS;
+        if (mEnableMultipleRegions) {
+            mQuickStepStartingRotation = info.rotation;
+        } else {
+            mActiveTouchRotation = 0;
+            mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
+        }
+        resetSwipeRegions(info);
+    }
+
+    /**
+     * Call when removing multiple regions to swipe from, but still in active quickswitch mode (task
+     * list is still frozen).
+     * Ex. This would be called when user has quickswitched to the same app rotation that
+     * they started quickswitching in, indicating that extra nav regions can be ignored. Calling
+     * this will update the value of {@link #mActiveTouchRotation}
+     *
+     * @param displayInfo The display whos rotation will be used as the current active rotation
+     */
+    void setSingleActiveRegion(DefaultDisplay.Info displayInfo) {
+        mActiveTouchRotation = displayInfo.rotation;
+        resetSwipeRegions(displayInfo);
+    }
+
+    /**
+     * Only saves the swipe region represented by {@param region}, clears the
+     * rest from {@link #mSwipeTouchRegions}
+     * To be called whenever we want to stop tracking more than one swipe region.
+     * Ok to call multiple times.
+     */
+    private void resetSwipeRegions(DefaultDisplay.Info region) {
+        if (DEBUG) {
+            Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplayRotation);
+        }
+
+        mCurrentDisplayRotation = region.rotation;
+        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        if (regionToKeep == null) {
+            regionToKeep = createRegionForDisplay(region);
+        }
+        mSwipeTouchRegions.clear();
+        mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+        updateAssistantRegions(regionToKeep);
+    }
+
+    private void resetSwipeRegions() {
+        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        mSwipeTouchRegions.clear();
+        if (regionToKeep != null) {
+            mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+            updateAssistantRegions(regionToKeep);
+        }
+    }
+
+    private OrientationRectF createRegionForDisplay(DefaultDisplay.Info display) {
+        if (DEBUG) {
+            Log.d(TAG, "creating rotation region for: " + mCurrentDisplayRotation);
+        }
+
+        Point size = display.realSize;
+        int rotation = display.rotation;
+        OrientationRectF orientationRectF =
+                new OrientationRectF(0, 0, size.x, size.y, rotation);
+        if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
+            int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+            orientationRectF.top = orientationRectF.bottom - touchHeight;
+            updateAssistantRegions(orientationRectF);
+        } else {
+            mAssistantLeftRegion.setEmpty();
+            mAssistantRightRegion.setEmpty();
+            switch (rotation) {
+                case Surface.ROTATION_90:
+                    orientationRectF.left = orientationRectF.right
+                            - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+                    break;
+                case Surface.ROTATION_270:
+                    orientationRectF.right = orientationRectF.left
+                            + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+                    break;
+                default:
+                    orientationRectF.top = orientationRectF.bottom
+                            - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+            }
+        }
+
+        return orientationRectF;
+    }
+
+    private void updateAssistantRegions(OrientationRectF orientationRectF) {
+        int navbarHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+        int assistantWidth = mResources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
+        float assistantHeight = Math.max(navbarHeight, mContractInfo.getWindowCornerRadius());
+        mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = orientationRectF.bottom;
+        mAssistantLeftRegion.top = mAssistantRightRegion.top =
+                orientationRectF.bottom - assistantHeight;
+
+        mAssistantLeftRegion.left = 0;
+        mAssistantLeftRegion.right = assistantWidth;
+
+        mAssistantRightRegion.right = orientationRectF.right;
+        mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
+    }
+
+    boolean touchInAssistantRegion(MotionEvent ev) {
+        return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
+                || mAssistantRightRegion.contains(ev.getX(), ev.getY());
+
+    }
+
+    private int getNavbarSize(String resName) {
+        return ResourceUtils.getNavbarSize(resName, mResources);
+    }
+
+    boolean touchInValidSwipeRegions(float x, float y) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "touchInValidSwipeRegions " + x + "," + y + " in "
+                    + mLastRectTouched);
+        }
+        if (mLastRectTouched != null) {
+            return mLastRectTouched.contains(x, y);
+        }
+        return false;
+    }
+
+    int getCurrentActiveRotation() {
+        return mActiveTouchRotation;
+    }
+
+    int getQuickStepStartingRotation() {
+        return mQuickStepStartingRotation;
+    }
+
+    public void transform(MotionEvent event) {
+        int eventAction = event.getActionMasked();
+        switch (eventAction) {
+            case ACTION_MOVE: {
+                if (mLastRectTouched == null) {
+                    return;
+                }
+                mLastRectTouched.applyTransform(event, true);
+                break;
+            }
+            case ACTION_CANCEL:
+            case ACTION_UP: {
+                if (mLastRectTouched == null) {
+                    return;
+                }
+                mLastRectTouched.applyTransform(event, true);
+                mLastRectTouched = null;
+                break;
+            }
+            case ACTION_POINTER_DOWN:
+            case ACTION_DOWN: {
+                if (mLastRectTouched != null) {
+                    return;
+                }
+
+                for (int i = 0; i < MAX_ORIENTATIONS; i++) {
+                    OrientationRectF rect = mSwipeTouchRegions.get(i);
+                    if (TestProtocol.sDebugTracing) {
+                        Log.d(TestProtocol.NO_SWIPE_TO_HOME, "transform:DOWN, rect=" + rect);
+                    }
+                    if (rect == null) {
+                        continue;
+                    }
+                    if (rect.applyTransform(event, false)) {
+                        if (TestProtocol.sDebugTracing) {
+                            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "setting mLastRectTouched");
+                        }
+                        mLastRectTouched = rect;
+                        mActiveTouchRotation = rect.mRotation;
+                        if (mEnableMultipleRegions
+                                && mCurrentDisplayRotation == mActiveTouchRotation) {
+                            // TODO(b/154580671) might make this block unnecessary
+                            // Start a touch session for the default nav region for the display
+                            mQuickStepStartingRotation = mLastRectTouched.mRotation;
+                            resetSwipeRegions();
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "set active region: " + rect);
+                        }
+                        return;
+                    }
+                }
+                break;
+            }
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("OrientationTouchTransformerState: ");
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        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));
+            regions.append(rectF.mRotation).append(" ");
+        }
+        pw.println(regions.toString());
+    }
+
+    private class OrientationRectF extends RectF {
+
+        private int mRotation;
+        private float mHeight;
+        private float mWidth;
+
+        OrientationRectF(float left, float top, float right, float bottom, int rotation) {
+            super(left, top, right, bottom);
+            this.mRotation = rotation;
+            mHeight = bottom;
+            mWidth = right;
+        }
+
+        @Override
+        public String toString() {
+            String s = super.toString();
+            s += " rotation: " + mRotation;
+            return s;
+        }
+
+        @Override
+        public boolean contains(float x, float y) {
+            // Mark bottom right as included in the Rect (copied from Rect src, added "=" in "<=")
+            return left < right && top < bottom  // check for empty first
+                    && x >= left && x <= right && y >= top && y <= bottom;
+        }
+
+        boolean applyTransform(MotionEvent event, boolean forceTransform) {
+            mTmpMatrix.reset();
+            postDisplayRotation(deltaRotation(mCurrentDisplayRotation, mRotation),
+                    mHeight, mWidth, mTmpMatrix);
+            if (forceTransform) {
+                if (DEBUG) {
+                    Log.d(TAG, "Transforming rotation due to forceTransform, "
+                            + "mCurrentRotation: " + mCurrentDisplayRotation
+                            + "mRotation: " + mRotation);
+                }
+                event.transform(mTmpMatrix);
+                return true;
+            }
+            mTmpPoint[0] = event.getX();
+            mTmpPoint[1] = event.getY();
+            mTmpMatrix.mapPoints(mTmpPoint);
+
+            if (DEBUG) {
+                Log.d(TAG, "original: " + event.getX() + ", " + event.getY()
+                                + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
+                                + " rect: " + this + " forceTransform: " + forceTransform
+                                + " contains: " + contains(mTmpPoint[0], mTmpPoint[1]));
+            }
+
+            if (contains(mTmpPoint[0], mTmpPoint[1])) {
+                event.transform(mTmpMatrix);
+                return true;
+            }
+            return false;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
deleted file mode 100644
index f5573ba..0000000
--- a/quickstep/src/com/android/quickstep/OverviewCallbacks.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.content.Context;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Callbacks related to overview/quicksteps.
- */
-public class OverviewCallbacks implements ResourceBasedOverride {
-
-    private static OverviewCallbacks sInstance;
-
-    public static OverviewCallbacks get(Context context) {
-        Preconditions.assertUIThread();
-        if (sInstance == null) {
-            sInstance = Overrides.getObject(OverviewCallbacks.class,
-                    context.getApplicationContext(), R.string.overview_callbacks_class);
-        }
-        return sInstance;
-    }
-
-    public void onInitOverviewTransition() { }
-
-    public void closeAllWindows() { }
-}
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 93a6127..07f838b 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -20,10 +20,10 @@
 import static android.content.Intent.ACTION_PACKAGE_CHANGED;
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 
+import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -35,43 +35,44 @@
 import android.content.pm.ResolveInfo;
 import android.util.SparseIntArray;
 
+import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 /**
  * Class to keep track of the current overview component based off user preferences and app updates
  * and provide callers the relevant classes.
  */
 public final class OverviewComponentObserver {
-    private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            updateOverviewTargets();
-        }
-    };
-    private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            updateOverviewTargets();
-        }
-    };
+    private final BroadcastReceiver mUserPreferenceChangeReceiver =
+            new SimpleBroadcastReceiver(this::updateOverviewTargets);
+    private final BroadcastReceiver mOtherHomeAppUpdateReceiver =
+            new SimpleBroadcastReceiver(this::updateOverviewTargets);
+
     private final Context mContext;
+    private final RecentsAnimationDeviceState mDeviceState;
     private final Intent mCurrentHomeIntent;
     private final Intent mMyHomeIntent;
     private final Intent mFallbackIntent;
     private final SparseIntArray mConfigChangesMap = new SparseIntArray();
+
+    private Consumer<Boolean> mOverviewChangeListener = b -> { };
+
     private String mUpdateRegisteredPackage;
-    private ActivityControlHelper mActivityControlHelper;
+    private BaseActivityInterface mActivityInterface;
     private Intent mOverviewIntent;
-    private int mSystemUiStateFlags;
     private boolean mIsHomeAndOverviewSame;
     private boolean mIsDefaultHome;
+    private boolean mIsHomeDisabled;
 
-    public OverviewComponentObserver(Context context) {
+
+    public OverviewComponentObserver(Context context, RecentsAnimationDeviceState deviceState) {
         mContext = context;
-
+        mDeviceState = deviceState;
         mCurrentHomeIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -99,53 +100,65 @@
         updateOverviewTargets();
     }
 
-    public void onSystemUiStateChanged(int stateFlags) {
-        boolean homeDisabledChanged = (mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED)
-                != (stateFlags & SYSUI_STATE_HOME_DISABLED);
-        mSystemUiStateFlags = stateFlags;
-        if (homeDisabledChanged) {
+    /**
+     * Sets a listener for changes in {@link #isHomeAndOverviewSame()}
+     */
+    public void setOverviewChangeListener(Consumer<Boolean> overviewChangeListener) {
+        mOverviewChangeListener = overviewChangeListener;
+    }
+
+    public void onSystemUiStateChanged() {
+        if (mDeviceState.isHomeDisabled() != mIsHomeDisabled) {
             updateOverviewTargets();
         }
     }
 
+    private void updateOverviewTargets(Intent unused) {
+        updateOverviewTargets();
+    }
+
     public boolean assistantGestureIsConstrained() {
-        return (mSystemUiStateFlags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0;
+        return (mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0;
     }
 
     /**
-     * Update overview intent and {@link ActivityControlHelper} based off the current launcher home
+     * Update overview intent and {@link BaseActivityInterface} based off the current launcher home
      * component.
      */
     private void updateOverviewTargets() {
         ComponentName defaultHome = PackageManagerWrapper.getInstance()
                 .getHomeActivities(new ArrayList<>());
 
+        mIsHomeDisabled = mDeviceState.isHomeDisabled();
         mIsDefaultHome = Objects.equals(mMyHomeIntent.getComponent(), defaultHome);
 
         // Set assistant visibility to 0 from launcher's perspective, ensures any elements that
         // launcher made invisible become visible again before the new activity control helper
         // becomes active.
-        if (mActivityControlHelper != null) {
-            mActivityControlHelper.onAssistantVisibilityChanged(0.f);
+        if (mActivityInterface != null) {
+            mActivityInterface.onAssistantVisibilityChanged(0.f);
         }
 
-        if ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
-                && (defaultHome == null || mIsDefaultHome)) {
+        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+            mIsDefaultHome = false;
+            if (defaultHome == null) {
+                defaultHome = mMyHomeIntent.getComponent();
+            }
+        }
+
+        if (!mDeviceState.isHomeDisabled() && (defaultHome == null || mIsDefaultHome)) {
             // User default home is same as out home app. Use Overview integrated in Launcher.
-            mActivityControlHelper = new LauncherActivityControllerHelper();
+            mActivityInterface = LauncherActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = true;
             mOverviewIntent = mMyHomeIntent;
             mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent());
 
-            if (mUpdateRegisteredPackage != null) {
-                // Remove any update listener as we don't care about other packages.
-                mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
-                mUpdateRegisteredPackage = null;
-            }
+            // Remove any update listener as we don't care about other packages.
+            unregisterOtherHomeAppUpdateReceiver();
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
 
-            mActivityControlHelper = new FallbackActivityControllerHelper();
+            mActivityInterface = FallbackActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = false;
             mOverviewIntent = mFallbackIntent;
             mCurrentHomeIntent.setComponent(defaultHome);
@@ -155,13 +168,9 @@
             // Listen for package updates of this app (and remove any previously attached
             // package listener).
             if (defaultHome == null) {
-                if (mUpdateRegisteredPackage != null) {
-                    mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
-                }
+                unregisterOtherHomeAppUpdateReceiver();
             } else if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) {
-                if (mUpdateRegisteredPackage != null) {
-                    mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
-                }
+                unregisterOtherHomeAppUpdateReceiver();
 
                 mUpdateRegisteredPackage = defaultHome.getPackageName();
                 mContext.registerReceiver(mOtherHomeAppUpdateReceiver, getPackageFilter(
@@ -169,6 +178,7 @@
                         ACTION_PACKAGE_REMOVED));
             }
         }
+        mOverviewChangeListener.accept(mIsHomeAndOverviewSame);
     }
 
     /**
@@ -176,7 +186,10 @@
      */
     public void onDestroy() {
         mContext.unregisterReceiver(mUserPreferenceChangeReceiver);
+        unregisterOtherHomeAppUpdateReceiver();
+    }
 
+    private void unregisterOtherHomeAppUpdateReceiver() {
         if (mUpdateRegisteredPackage != null) {
             mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
             mUpdateRegisteredPackage = null;
@@ -237,7 +250,16 @@
      *
      * @return the current activity control helper
      */
-    public ActivityControlHelper getActivityControlHelper() {
-        return mActivityControlHelper;
+    public BaseActivityInterface getActivityInterface() {
+        return mActivityInterface;
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("OverviewComponentObserver:");
+        pw.println("  isDefaultHome=" + mIsDefaultHome);
+        pw.println("  isHomeDisabled=" + mIsHomeDisabled);
+        pw.println("  homeAndOverviewSame=" + mIsHomeAndOverviewSame);
+        pw.println("  overviewIntent=" + mOverviewIntent);
+        pw.println("  homeIntent=" + mCurrentHomeIntent);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java
deleted file mode 100644
index 858c3b6..0000000
--- a/quickstep/src/com/android/quickstep/OverviewInteractionState.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-
-/**
- * Sets alpha for the back button
- */
-public class OverviewInteractionState {
-
-    private static final String TAG = "OverviewFlags";
-
-    private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
-
-    // We do not need any synchronization for this variable as its only written on UI thread.
-    public static final MainThreadInitializedObject<OverviewInteractionState> INSTANCE =
-            new MainThreadInitializedObject<>(OverviewInteractionState::new);
-
-    private static final int MSG_SET_PROXY = 200;
-    private static final int MSG_SET_BACK_BUTTON_ALPHA = 201;
-
-    private final Context mContext;
-    private final Handler mUiHandler;
-    private final Handler mBgHandler;
-
-    // These are updated on the background thread
-    private ISystemUiProxy mISystemUiProxy;
-    private float mBackButtonAlpha = 1;
-
-    private int mSystemUiStateFlags;
-
-    private OverviewInteractionState(Context context) {
-        mContext = context;
-
-        // Data posted to the uihandler will be sent to the bghandler. Data is sent to uihandler
-        // because of its high send frequency and data may be very different than the previous value
-        // For example, send back alpha on uihandler to avoid flickering when setting its visibility
-        mUiHandler = new Handler(this::handleUiMessage);
-        mBgHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleBgMessage);
-
-        onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(context)
-                .addModeChangeListener(this::onNavigationModeChanged));
-    }
-
-    public float getBackButtonAlpha() {
-        return mBackButtonAlpha;
-    }
-
-    public void setBackButtonAlpha(float alpha, boolean animate) {
-        if (!modeSupportsGestures()) {
-            alpha = 1;
-        }
-        mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_ALPHA);
-        mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_ALPHA, animate ? 1 : 0, 0, alpha)
-                .sendToTarget();
-    }
-
-    public void setSystemUiProxy(ISystemUiProxy proxy) {
-        mBgHandler.obtainMessage(MSG_SET_PROXY, proxy).sendToTarget();
-    }
-
-    public void setSystemUiStateFlags(int stateFlags) {
-        mSystemUiStateFlags = stateFlags;
-    }
-
-    public int getSystemUiStateFlags() {
-        return mSystemUiStateFlags;
-    }
-
-    private boolean handleUiMessage(Message msg) {
-        if (msg.what == MSG_SET_BACK_BUTTON_ALPHA) {
-            mBackButtonAlpha = (float) msg.obj;
-        }
-        mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2, msg.obj).sendToTarget();
-        return true;
-    }
-
-    private boolean handleBgMessage(Message msg) {
-        switch (msg.what) {
-            case MSG_SET_PROXY:
-                mISystemUiProxy = (ISystemUiProxy) msg.obj;
-                break;
-            case MSG_SET_BACK_BUTTON_ALPHA:
-                applyBackButtonAlpha((float) msg.obj, msg.arg1 == 1);
-                return true;
-        }
-        return true;
-    }
-
-    @WorkerThread
-    private void applyBackButtonAlpha(float alpha, boolean animate) {
-        if (mISystemUiProxy == null) {
-            return;
-        }
-        try {
-            mISystemUiProxy.setBackButtonAlpha(alpha, animate);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Unable to update overview back button alpha", e);
-        }
-    }
-
-    private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
-        resetHomeBounceSeenOnQuickstepEnabledFirstTime();
-    }
-
-    private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
-        if (modeSupportsGestures() && !Utilities.getPrefs(mContext).getBoolean(
-                HAS_ENABLED_QUICKSTEP_ONCE, true)) {
-            Utilities.getPrefs(mContext).edit()
-                .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
-                .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false)
-                .apply();
-        }
-    }
-
-    private boolean modeSupportsGestures() {
-        return SysUINavigationMode.getMode(mContext).hasGestures;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index befeee0..32268a4 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -49,6 +49,7 @@
         super.init(context);
 
         // Elevate GPU priority for Quickstep and Remote animations.
-        ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+        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 10f9feb..70b4f20 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -40,21 +40,20 @@
 /**
  * Manages the recent task list from the system, caching it as necessary.
  */
-@TargetApi(Build.VERSION_CODES.P)
+@TargetApi(Build.VERSION_CODES.R)
 public class RecentTasksList extends TaskStackChangeListener {
 
+    private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
+
     private final KeyguardManagerCompat mKeyguardManager;
     private final LooperExecutor mMainThreadExecutor;
     private final ActivityManagerWrapper mActivityManagerWrapper;
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
-    // The last change id when the list was last loaded completely, must be <= the list change id
-    private int mLastLoadedId;
-    // The last change id was loaded with keysOnly  = true
-    private boolean mLastLoadHadKeysOnly;
 
-    ArrayList<Task> mTasks = new ArrayList<>();
+    private TaskLoadResult mResultsBg = INVALID_RESULT;
+    private TaskLoadResult mResultsUi = INVALID_RESULT;
 
     public RecentTasksList(LooperExecutor mainThreadExecutor,
             KeyguardManagerCompat keyguardManager, ActivityManagerWrapper activityManagerWrapper) {
@@ -71,7 +70,7 @@
     public void getTaskKeys(int numTasks, Consumer<ArrayList<Task>> callback) {
         // Kick off task loading in the background
         UI_HELPER_EXECUTOR.execute(() -> {
-            ArrayList<Task> tasks = loadTasksInBackground(numTasks, true /* loadKeysOnly */);
+            ArrayList<Task> tasks = loadTasksInBackground(numTasks, -1, true /* loadKeysOnly */);
             mMainThreadExecutor.execute(() -> callback.accept(tasks));
         });
     }
@@ -85,26 +84,30 @@
      */
     public synchronized int getTasks(boolean loadKeysOnly, Consumer<ArrayList<Task>> callback) {
         final int requestLoadId = mChangeId;
-        Runnable resultCallback = callback == null
-                ? () -> { }
-                : () -> callback.accept(copyOf(mTasks));
-
-        if (mLastLoadedId == mChangeId && (!mLastLoadHadKeysOnly || loadKeysOnly)) {
+        if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
             // The list is up to date, send the callback on the next frame,
             // so that requestID can be returned first.
-            mMainThreadExecutor.post(resultCallback);
+            if (callback != null) {
+                // Copy synchronously as the changeId might change by next frame
+                ArrayList<Task> result = copyOf(mResultsUi);
+                mMainThreadExecutor.post(() -> callback.accept(result));
+            }
+
             return requestLoadId;
         }
 
         // Kick off task loading in the background
         UI_HELPER_EXECUTOR.execute(() -> {
-            ArrayList<Task> tasks = loadTasksInBackground(Integer.MAX_VALUE, loadKeysOnly);
-
+            if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
+                mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
+            }
+            TaskLoadResult loadResult = mResultsBg;
             mMainThreadExecutor.execute(() -> {
-                mTasks = tasks;
-                mLastLoadedId = requestLoadId;
-                mLastLoadHadKeysOnly = loadKeysOnly;
-                resultCallback.run();
+                mResultsUi = loadResult;
+                if (callback != null) {
+                    ArrayList<Task> result = copyOf(mResultsUi);
+                    callback.accept(result);
+                }
             });
         });
 
@@ -119,23 +122,40 @@
     }
 
     @Override
-    public synchronized void onTaskStackChanged() {
-        mChangeId++;
+    public void onTaskStackChanged() {
+        invalidateLoadedTasks();
+    }
+
+    @Override
+    public void onRecentTaskListUpdated() {
+        // In some cases immediately after booting, the tasks in the system recent task list may be
+        // loaded, but not in the active task hierarchy in the system.  These tasks are displayed in 
+        // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
+        // callback (those are for changes to the active tasks), but the task list is still updated,
+        // so we should also invalidate the change id to ensure we load a new list instead of 
+        // reusing a stale list.
+        invalidateLoadedTasks();
     }
 
     @Override
     public void onTaskRemoved(int taskId) {
-        mTasks = loadTasksInBackground(Integer.MAX_VALUE, false);
+        invalidateLoadedTasks();
     }
 
+
     @Override
-    public synchronized void onActivityPinned(String packageName, int userId, int taskId,
-            int stackId) {
-        mChangeId++;
+    public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
+        invalidateLoadedTasks();
     }
 
     @Override
     public synchronized void onActivityUnpinned() {
+        invalidateLoadedTasks();
+    }
+
+    private synchronized void invalidateLoadedTasks() {
+        UI_HELPER_EXECUTOR.execute(() -> mResultsBg = INVALID_RESULT);
+        mResultsUi = INVALID_RESULT;
         mChangeId++;
     }
 
@@ -143,10 +163,8 @@
      * Loads and creates a list of all the recent tasks.
      */
     @VisibleForTesting
-    ArrayList<Task> loadTasksInBackground(int numTasks,
-            boolean loadKeysOnly) {
+    TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<Task> allTasks = new ArrayList<>();
         List<ActivityManager.RecentTaskInfo> rawTasks =
                 mActivityManagerWrapper.getRecentTasks(numTasks, currentUserId);
         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
@@ -163,9 +181,8 @@
             }
         };
 
-        int taskCount = rawTasks.size();
-        for (int i = 0; i < taskCount; i++) {
-            ActivityManager.RecentTaskInfo rawTask = rawTasks.get(i);
+        TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
+        for (ActivityManager.RecentTaskInfo rawTask : rawTasks) {
             Task.TaskKey taskKey = new Task.TaskKey(rawTask);
             Task task;
             if (!loadKeysOnly) {
@@ -189,4 +206,22 @@
         }
         return newTasks;
     }
+
+    private static class TaskLoadResult extends ArrayList<Task> {
+
+        final int mId;
+
+        // If the result was loaded with keysOnly  = true
+        final boolean mKeysOnly;
+
+        TaskLoadResult(int id, boolean keysOnly, int size) {
+            super(size);
+            mId = id;
+            mKeysOnly = keysOnly;
+        }
+
+        boolean isValidForRequest(int requestId, boolean loadKeysOnly) {
+            return mId == requestId && (!mKeysOnly || loadKeysOnly);
+        }
+    }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
deleted file mode 100644
index 4d1d9ef..0000000
--- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
-import com.android.quickstep.util.RemoteAnimationProvider;
-
-import java.lang.ref.WeakReference;
-import java.util.function.BiPredicate;
-
-/**
- * Utility class to track create/destroy for some {@link BaseRecentsActivity}.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class RecentsActivityTracker<T extends BaseRecentsActivity> implements ActivityInitListener {
-
-    private static WeakReference<BaseRecentsActivity> sCurrentActivity =
-            new WeakReference<>(null);
-    private static final Scheduler sScheduler = new Scheduler();
-
-    private final BiPredicate<T, Boolean> mOnInitListener;
-
-    public RecentsActivityTracker(BiPredicate<T, Boolean> onInitListener) {
-        mOnInitListener = onInitListener;
-    }
-
-    @Override
-    public void register() {
-        sScheduler.schedule(this);
-    }
-
-    @Override
-    public void unregister() {
-        sScheduler.clearReference(this);
-    }
-
-    private boolean init(T activity, boolean visible) {
-        return mOnInitListener.test(activity, visible);
-    }
-
-    public static <T extends BaseRecentsActivity> T getCurrentActivity() {
-        return (T) sCurrentActivity.get();
-    }
-
-    @Override
-    public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
-            Context context, Handler handler, long duration) {
-        register();
-
-        Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
-        context.startActivity(intent, options);
-    }
-
-    public static void onRecentsActivityCreate(BaseRecentsActivity activity) {
-        sCurrentActivity = new WeakReference<>(activity);
-        sScheduler.initIfPending(activity, false);
-    }
-
-
-    public static void onRecentsActivityNewIntent(BaseRecentsActivity activity) {
-        sScheduler.initIfPending(activity, activity.isStarted());
-    }
-
-    public static void onRecentsActivityDestroy(BaseRecentsActivity activity) {
-        if (sCurrentActivity.get() == activity) {
-            sCurrentActivity.clear();
-        }
-    }
-
-
-    private static class Scheduler implements Runnable {
-
-        private WeakReference<RecentsActivityTracker> mPendingTracker = new WeakReference<>(null);
-
-        public synchronized void schedule(RecentsActivityTracker tracker) {
-            mPendingTracker = new WeakReference<>(tracker);
-            MAIN_EXECUTOR.execute(this);
-        }
-
-        @Override
-        public void run() {
-            BaseRecentsActivity activity = sCurrentActivity.get();
-            if (activity != null) {
-                initIfPending(activity, activity.isStarted());
-            }
-        }
-
-        public synchronized boolean initIfPending(BaseRecentsActivity activity,
-                boolean alreadyOnHome) {
-            RecentsActivityTracker tracker = mPendingTracker.get();
-            if (tracker != null) {
-                if (!tracker.init(activity, alreadyOnHome)) {
-                    mPendingTracker.clear();
-                }
-                return true;
-            }
-            return false;
-        }
-
-        public synchronized boolean clearReference(RecentsActivityTracker tracker) {
-            if (mPendingTracker.get() == tracker) {
-                mPendingTracker.clear();
-                return true;
-            }
-            return false;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
new file mode 100644
index 0000000..a21c714
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.graphics.Rect;
+import android.util.ArraySet;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Preconditions;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.Set;
+
+/**
+ * Wrapper around {@link com.android.systemui.shared.system.RecentsAnimationListener} which
+ * delegates callbacks to multiple listeners on the main thread
+ */
+public class RecentsAnimationCallbacks implements
+        com.android.systemui.shared.system.RecentsAnimationListener {
+
+    private final Set<RecentsAnimationListener> mListeners = new ArraySet<>();
+    private final boolean mAllowMinimizeSplitScreen;
+
+    // TODO(141886704): Remove these references when they are no longer needed
+    private RecentsAnimationController mController;
+
+    private boolean mCancelled;
+
+    public RecentsAnimationCallbacks(boolean allowMinimizeSplitScreen) {
+        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
+    }
+
+    @UiThread
+    public void addListener(RecentsAnimationListener listener) {
+        Preconditions.assertUIThread();
+        mListeners.add(listener);
+    }
+
+    @UiThread
+    public void removeListener(RecentsAnimationListener listener) {
+        Preconditions.assertUIThread();
+        mListeners.remove(listener);
+    }
+
+    @UiThread
+    public void removeAllListeners() {
+        Preconditions.assertUIThread();
+        mListeners.clear();
+    }
+
+    public void notifyAnimationCanceled() {
+        mCancelled = true;
+        onAnimationCanceled(null);
+    }
+
+    // Called only in Q platform
+    @BinderThread
+    @Deprecated
+    public final void onAnimationStart(RecentsAnimationControllerCompat controller,
+            RemoteAnimationTargetCompat[] appTargets, Rect homeContentInsets,
+            Rect minimizedHomeBounds) {
+        onAnimationStart(controller, appTargets, new RemoteAnimationTargetCompat[0],
+                homeContentInsets, minimizedHomeBounds);
+    }
+
+    // Called only in R+ platform
+    @BinderThread
+    public final void onAnimationStart(RecentsAnimationControllerCompat animationController,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            Rect homeContentInsets, Rect minimizedHomeBounds) {
+        RecentsAnimationTargets targets = new RecentsAnimationTargets(appTargets,
+                wallpaperTargets, homeContentInsets, minimizedHomeBounds);
+        mController = new RecentsAnimationController(animationController,
+                mAllowMinimizeSplitScreen, this::onAnimationFinished);
+
+        if (mCancelled) {
+            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
+                    mController::finishAnimationToApp);
+        } else {
+            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+                for (RecentsAnimationListener listener : getListeners()) {
+                    listener.onRecentsAnimationStart(mController, targets);
+                }
+            });
+        }
+    }
+
+    @BinderThread
+    @Override
+    public final void onAnimationCanceled(ThumbnailData thumbnailData) {
+        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+            for (RecentsAnimationListener listener : getListeners()) {
+                listener.onRecentsAnimationCanceled(thumbnailData);
+            }
+        });
+    }
+
+    @BinderThread
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat app) {
+        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+            for (RecentsAnimationListener listener : getListeners()) {
+                listener.onTaskAppeared(app);
+            }
+        });
+    }
+
+    private final void onAnimationFinished(RecentsAnimationController controller) {
+        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+            for (RecentsAnimationListener listener : getListeners()) {
+                listener.onRecentsAnimationFinished(controller);
+            }
+        });
+    }
+
+    private RecentsAnimationListener[] getListeners() {
+        return mListeners.toArray(new RecentsAnimationListener[mListeners.size()]);
+    }
+
+    /**
+     * Listener for the recents animation callbacks.
+     */
+    public interface RecentsAnimationListener {
+        default void onRecentsAnimationStart(RecentsAnimationController controller,
+                RecentsAnimationTargets targets) {}
+
+        /**
+         * Callback from the system when the recents animation is canceled. {@param thumbnailData}
+         * is passed back for rendering screenshot to replace live tile.
+         */
+        default void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {}
+
+        /**
+         * Callback made whenever the recents animation is finished.
+         */
+        default void onRecentsAnimationFinished(RecentsAnimationController controller) {}
+
+        /**
+         * Callback made when a task started from the recents is ready for an app transition.
+         */
+        default void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {}
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
new file mode 100644
index 0000000..4e9aa61
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -0,0 +1,271 @@
+/*
+ * 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.quickstep;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.util.Preconditions;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Wrapper around RecentsAnimationControllerCompat to help with some synchronization
+ */
+public class RecentsAnimationController {
+
+    private static final String TAG = "RecentsAnimationController";
+
+    private final RecentsAnimationControllerCompat mController;
+    private final Consumer<RecentsAnimationController> mOnFinishedListener;
+    private final boolean mAllowMinimizeSplitScreen;
+
+    private InputConsumerController mInputConsumerController;
+    private Supplier<InputConsumer> mInputProxySupplier;
+    private InputConsumer mInputConsumer;
+    private boolean mUseLauncherSysBarFlags = false;
+    private boolean mSplitScreenMinimized = false;
+    private boolean mTouchInProgress;
+    private boolean mDisableInputProxyPending;
+
+    public RecentsAnimationController(RecentsAnimationControllerCompat controller,
+            boolean allowMinimizeSplitScreen,
+            Consumer<RecentsAnimationController> onFinishedListener) {
+        mController = controller;
+        mOnFinishedListener = onFinishedListener;
+        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
+    }
+
+    /**
+     * Synchronously takes a screenshot of the task with the given {@param taskId} if the task is
+     * currently being animated.
+     */
+    public ThumbnailData screenshotTask(int taskId) {
+        return mController.screenshotTask(taskId);
+    }
+
+    /**
+     * Indicates that the gesture has crossed the window boundary threshold and system UI can be
+     * update the system bar flags accordingly.
+     */
+    public void setUseLauncherSystemBarFlags(boolean useLauncherSysBarFlags) {
+        if (mUseLauncherSysBarFlags != useLauncherSysBarFlags) {
+            mUseLauncherSysBarFlags = useLauncherSysBarFlags;
+            UI_HELPER_EXECUTOR.execute(() -> {
+                mController.setAnimationTargetsBehindSystemBars(!useLauncherSysBarFlags);
+            });
+        }
+    }
+
+    /**
+     * Indicates that the gesture has crossed the window boundary threshold and we should minimize
+     * if we are in splitscreen.
+     */
+    public void setSplitScreenMinimized(boolean splitScreenMinimized) {
+        if (!mAllowMinimizeSplitScreen) {
+            return;
+        }
+        if (mSplitScreenMinimized != splitScreenMinimized) {
+            mSplitScreenMinimized = splitScreenMinimized;
+            UI_HELPER_EXECUTOR.execute(() -> {
+                SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
+                if (p != null) {
+                    p.setSplitScreenMinimized(splitScreenMinimized);
+                }
+            });
+        }
+    }
+
+    /**
+     * Notifies the controller that we want to defer cancel until the next app transition starts.
+     * If {@param screenshot} is set, then we will receive a screenshot on the next
+     * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)} and we must also call
+     * {@link #cleanupScreenshot()} when that screenshot is no longer used.
+     */
+    public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
+        mController.setDeferCancelUntilNextTransition(defer, screenshot);
+    }
+
+    /**
+     * Cleans up the screenshot previously returned from
+     * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)}.
+     */
+    public void cleanupScreenshot() {
+        UI_HELPER_EXECUTOR.execute(() -> mController.cleanupScreenshot());
+    }
+
+    /**
+     * Remove task remote animation target from
+     * {@link RecentsAnimationCallbacks#onTaskAppeared(RemoteAnimationTargetCompat)}}.
+     */
+    @UiThread
+    public boolean removeTaskTarget(@NonNull RemoteAnimationTargetCompat target) {
+        return mController.removeTask(target.taskId);
+    }
+
+    @UiThread
+    public void finishAnimationToHome() {
+        finishAndDisableInputProxy(true /* toRecents */, null, false /* sendUserLeaveHint */);
+    }
+
+    @UiThread
+    public void finishAnimationToApp() {
+        finishAndDisableInputProxy(false /* toRecents */, null, false /* sendUserLeaveHint */);
+    }
+
+    /** See {@link #finish(boolean, Runnable, boolean)} */
+    @UiThread
+    public void finish(boolean toRecents, Runnable onFinishComplete) {
+        finish(toRecents, onFinishComplete, false /* sendUserLeaveHint */);
+    }
+
+    /**
+     * @param onFinishComplete A callback that runs on the main thread after the animation
+     *                         controller has finished on the background thread.
+     * @param sendUserLeaveHint Determines whether userLeaveHint flag will be set on the pausing
+     *                          activity. If userLeaveHint is true, the activity will enter into
+     *                          picture-in-picture mode upon being paused.
+     */
+    @UiThread
+    public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
+        Preconditions.assertUIThread();
+        if (toRecents && mTouchInProgress) {
+            // Finish the controller as requested, but don't disable input proxy yet.
+            mDisableInputProxyPending = true;
+            finishController(toRecents, onFinishComplete, sendUserLeaveHint);
+        } else {
+            finishAndDisableInputProxy(toRecents, onFinishComplete, sendUserLeaveHint);
+        }
+    }
+
+    private void finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete,
+            boolean sendUserLeaveHint) {
+        disableInputProxy();
+        finishController(toRecents, onFinishComplete, sendUserLeaveHint);
+    }
+
+    @UiThread
+    public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
+        mOnFinishedListener.accept(this);
+        UI_HELPER_EXECUTOR.execute(() -> {
+            mController.setInputConsumerEnabled(false);
+            mController.finish(toRecents, sendUserLeaveHint);
+            if (callback != null) {
+                MAIN_EXECUTOR.execute(callback);
+            }
+        });
+    }
+
+    /**
+     * Enables the input consumer to start intercepting touches in the app window.
+     */
+    public void enableInputConsumer() {
+        UI_HELPER_EXECUTOR.submit(() -> {
+            mController.hideCurrentInputMethod();
+            mController.setInputConsumerEnabled(true);
+        });
+    }
+
+    public void enableInputProxy(InputConsumerController inputConsumerController,
+            Supplier<InputConsumer> inputProxySupplier) {
+        mInputProxySupplier = inputProxySupplier;
+        mInputConsumerController = inputConsumerController;
+        mInputConsumerController.setInputListener(this::onInputConsumerEvent);
+    }
+
+    /** @return wrapper controller. */
+    public RecentsAnimationControllerCompat getController() {
+        return mController;
+    }
+
+    private void disableInputProxy() {
+        if (mInputConsumer != null && mTouchInProgress) {
+            long now = SystemClock.uptimeMillis();
+            MotionEvent dummyCancel = MotionEvent.obtain(now,  now, ACTION_CANCEL, 0, 0, 0);
+            mInputConsumer.onMotionEvent(dummyCancel);
+            dummyCancel.recycle();
+        }
+        if (mInputConsumerController != null) {
+            mInputConsumerController.setInputListener(null);
+        }
+        mInputProxySupplier = null;
+    }
+
+    private boolean onInputConsumerEvent(InputEvent ev) {
+        if (ev instanceof MotionEvent) {
+            onInputConsumerMotionEvent((MotionEvent) ev);
+        } else if (ev instanceof KeyEvent) {
+            if (mInputConsumer == null) {
+                mInputConsumer = mInputProxySupplier.get();
+            }
+            mInputConsumer.onKeyEvent((KeyEvent) ev);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean onInputConsumerMotionEvent(MotionEvent ev) {
+        int action = ev.getAction();
+
+        // Just to be safe, verify that ACTION_DOWN comes before any other action,
+        // and ignore any ACTION_DOWN after the first one (though that should not happen).
+        if (!mTouchInProgress && action != ACTION_DOWN) {
+            Log.w(TAG, "Received non-down motion before down motion: " + action);
+            return false;
+        }
+        if (mTouchInProgress && action == ACTION_DOWN) {
+            Log.w(TAG, "Received down motion while touch was already in progress");
+            return false;
+        }
+
+        if (action == ACTION_DOWN) {
+            mTouchInProgress = true;
+            if (mInputConsumer == null) {
+                mInputConsumer = mInputProxySupplier.get();
+            }
+        } else if (action == ACTION_CANCEL || action == ACTION_UP) {
+            // Finish any pending actions
+            mTouchInProgress = false;
+            if (mDisableInputProxyPending) {
+                mDisableInputProxyPending = false;
+                disableInputProxy();
+            }
+        }
+        if (mInputConsumer != null) {
+            mInputConsumer.onMotionEvent(ev);
+        }
+
+        return true;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
new file mode 100644
index 0000000..0a70bd6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static android.content.Intent.ACTION_USER_UNLOCKED;
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_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_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;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Region;
+import android.os.Process;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+
+import androidx.annotation.BinderThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Manages the state of the system during a swipe up gesture.
+ */
+public class RecentsAnimationDeviceState implements
+        NavigationModeChangeListener,
+        DefaultDisplay.DisplayInfoChangeListener {
+
+    private final Context mContext;
+    private final SysUINavigationMode mSysUiNavMode;
+    private final DefaultDisplay mDefaultDisplay;
+    private final int mDisplayId;
+    private int mDisplayRotation;
+
+    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+
+    private @SystemUiStateFlags int mSystemUiStateFlags;
+    private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
+    private NavBarPosition mNavBarPosition;
+
+    private final Region mDeferredGestureRegion = new Region();
+    private boolean mAssistantAvailable;
+    private float mAssistantVisibility;
+
+    private boolean mIsUserUnlocked;
+    private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
+    private final BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                mIsUserUnlocked = true;
+                notifyUserUnlocked();
+            }
+        }
+    };
+
+    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
+        @Override
+        public void onRecentTaskListFrozenChanged(boolean frozen) {
+            mTaskListFrozen = frozen;
+            if (frozen || mInOverview) {
+                return;
+            }
+            enableMultipleRegions(false);
+        }
+
+        @Override
+        public void onActivityRotation(int displayId) {
+            super.onActivityRotation(displayId);
+            // This always gets called before onDisplayInfoChanged() so we know how to process
+            // the rotation in that method. This is done to avoid having a race condition between
+            // the sensor readings and onDisplayInfoChanged() call
+            if (displayId != mDisplayId) {
+                return;
+            }
+
+            mPrioritizeDeviceRotation = true;
+            if (mInOverview) {
+                // reset, launcher must be rotating
+                mExitOverviewRunnable.run();
+            }
+        }
+    };
+
+    private Runnable mExitOverviewRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mInOverview = false;
+            enableMultipleRegions(false);
+        }
+    };
+
+    private OrientationTouchTransformer mOrientationTouchTransformer;
+    /**
+     * Used to listen for when the device rotates into the orientation of the current
+     * foreground app. For example, if a user quickswitches from a portrait to a fixed landscape
+     * app and then rotates rotates the device to match that orientation, this triggers calls to
+     * sysui to adjust the navbar.
+     */
+    private OrientationEventListener mOrientationListener;
+    private int mSensorRotation = ROTATION_0;
+    /**
+     * This is the configuration of the foreground app or the app that will be in the foreground
+     * once a quickstep gesture finishes.
+     */
+    private int mCurrentAppRotation = -1;
+    /**
+     * This flag is set to true when the device physically changes orientations. When true,
+     * we will always report the current rotation of the foreground app whenever the display
+     * changes, as it would indicate the user's intention to rotate the foreground app.
+     */
+    private boolean mPrioritizeDeviceRotation = false;
+
+    private Region mExclusionRegion;
+    private SystemGestureExclusionListenerCompat mExclusionListener;
+
+    private final List<ComponentName> mGestureBlockedActivities;
+    private Runnable mOnDestroyFrozenTaskRunnable;
+    /**
+     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
+     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
+     * they went into recents with as opposed to defaulting to the default display rotation.
+     * TODO: (b/156984037) For when user rotates after entering overview
+     */
+    private boolean mInOverview;
+    private boolean mTaskListFrozen;
+
+    private boolean mIsUserSetupComplete;
+
+    public RecentsAnimationDeviceState(Context context) {
+        mContext = context;
+        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
+        mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
+        mDisplayId = mDefaultDisplay.getInfo().id;
+        runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
+
+        // Register for user unlocked if necessary
+        mIsUserUnlocked = context.getSystemService(UserManager.class)
+                .isUserUnlocked(Process.myUserHandle());
+        if (!mIsUserUnlocked) {
+            mContext.registerReceiver(mUserUnlockedReceiver,
+                    new IntentFilter(ACTION_USER_UNLOCKED));
+        }
+        runOnDestroy(() -> Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver));
+
+        // Register for exclusion updates
+        mExclusionListener = new SystemGestureExclusionListenerCompat(mDisplayId) {
+            @Override
+            @BinderThread
+            public void onExclusionChanged(Region region) {
+                // Assignments are atomic, it should be safe on binder thread
+                mExclusionRegion = region;
+            }
+        };
+        runOnDestroy(mExclusionListener::unregister);
+
+        Resources resources = mContext.getResources();
+        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
+                () -> QuickStepContract.getWindowCornerRadius(resources));
+
+        // Register for navigation mode changes
+        onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
+
+        // Add any blocked activities
+        String[] blockingActivities;
+        try {
+            blockingActivities =
+                    context.getResources().getStringArray(R.array.gesture_blocking_activities);
+        } catch (Resources.NotFoundException e) {
+            blockingActivities = new String[0];
+        }
+        mGestureBlockedActivities = new ArrayList<>(blockingActivities.length);
+        for (String blockingActivity : blockingActivities) {
+            if (!TextUtils.isEmpty(blockingActivity)) {
+                mGestureBlockedActivities.add(
+                        ComponentName.unflattenFromString(blockingActivity));
+            }
+        }
+
+        SecureSettingsObserver userSetupObserver = new SecureSettingsObserver(
+                context.getContentResolver(),
+                e -> mIsUserSetupComplete = e,
+                Settings.Secure.USER_SETUP_COMPLETE,
+                0);
+        mIsUserSetupComplete = userSetupObserver.getValue();
+        if (!mIsUserSetupComplete) {
+            userSetupObserver.register();
+            runOnDestroy(userSetupObserver::unregister);
+        }
+
+        mOrientationListener = new OrientationEventListener(context) {
+            @Override
+            public void onOrientationChanged(int degrees) {
+                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
+                        mSensorRotation);
+                if (newRotation == mSensorRotation) {
+                    return;
+                }
+
+                mSensorRotation = newRotation;
+                mPrioritizeDeviceRotation = true;
+
+                if (newRotation == mCurrentAppRotation) {
+                    // When user rotates device to the orientation of the foreground app after
+                    // quickstepping
+                    toggleSecondaryNavBarsForRotation();
+                }
+            }
+        };
+    }
+
+    private void setupOrientationSwipeHandler() {
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
+        mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
+                .unregisterTaskStackListener(mFrozenTaskListener);
+        runOnDestroy(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void destroyOrientationSwipeHandlerCallback() {
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
+        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void runOnDestroy(Runnable action) {
+        mOnDestroyActions.add(action);
+    }
+
+    /**
+     * Cleans up all the registered listeners and receivers.
+     */
+    public void destroy() {
+        for (Runnable r : mOnDestroyActions) {
+            r.run();
+        }
+    }
+
+    /**
+     * Adds a listener for the nav mode change, guaranteed to be called after the device state's
+     * mode has changed.
+     */
+    public void addNavigationModeChangedCallback(NavigationModeChangeListener listener) {
+        listener.onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(listener));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(listener));
+    }
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        mDefaultDisplay.removeChangeListener(this);
+        mDefaultDisplay.addChangeListener(this);
+        onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
+
+        if (newMode == NO_BUTTON) {
+            mExclusionListener.register();
+        } else {
+            mExclusionListener.unregister();
+        }
+
+        mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
+
+        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
+        if (!mMode.hasGestures && newMode.hasGestures) {
+            setupOrientationSwipeHandler();
+        } else if (mMode.hasGestures && !newMode.hasGestures){
+            destroyOrientationSwipeHandlerCallback();
+        }
+
+        mMode = newMode;
+    }
+
+    @Override
+    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+        if (info.id != getDisplayId() || flags == CHANGE_FRAME_DELAY) {
+            // ignore displays that aren't running launcher and frame refresh rate changes
+            return;
+        }
+
+        mDisplayRotation = info.rotation;
+
+        if (!mMode.hasGestures) {
+            return;
+        }
+        mNavBarPosition = new NavBarPosition(mMode, info);
+        updateGestureTouchRegions();
+        mOrientationTouchTransformer.createOrAddTouchRegion(info);
+        mCurrentAppRotation = mDisplayRotation;
+
+        /* Update nav bars on the following:
+         * a) if this is coming from an activity rotation OR
+         *   aa) we launch an app in the orientation that user is already in
+         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
+         * c) We're actively in quickswitch mode
+         */
+        if ((mPrioritizeDeviceRotation
+                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
+                && !mInOverview
+                && mTaskListFrozen) {
+            toggleSecondaryNavBarsForRotation();
+        }
+    }
+
+    /**
+     * @return the current navigation mode for the device.
+     */
+    public SysUINavigationMode.Mode getNavMode() {
+        return mMode;
+    }
+
+    /**
+     * @return the nav bar position for the current nav bar mode and display rotation.
+     */
+    public NavBarPosition getNavBarPosition() {
+        return mNavBarPosition;
+    }
+
+    /**
+     * @return whether the current nav mode is fully gestural.
+     */
+    public boolean isFullyGesturalNavMode() {
+        return mMode == NO_BUTTON;
+    }
+
+    /**
+     * @return whether the current nav mode has some gestures (either 2 or 0 button mode).
+     */
+    public boolean isGesturalNavMode() {
+        return mMode == TWO_BUTTONS || mMode == NO_BUTTON;
+    }
+
+    /**
+     * @return whether the current nav mode is button-based.
+     */
+    public boolean isButtonNavMode() {
+        return mMode == THREE_BUTTONS;
+    }
+
+    /**
+     * @return the display id for the display that Launcher is running on.
+     */
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
+    /**
+     * Adds a callback for when a user is unlocked. If the user is already unlocked, this listener
+     * will be called back immediately.
+     */
+    public void runOnUserUnlocked(Runnable action) {
+        if (mIsUserUnlocked) {
+            action.run();
+        } else {
+            mUserUnlockedActions.add(action);
+        }
+    }
+
+    /**
+     * @return whether the user is unlocked.
+     */
+    public boolean isUserUnlocked() {
+        return mIsUserUnlocked;
+    }
+
+    /**
+     * @return whether the user has completed setup wizard
+     */
+    public boolean isUserSetupComplete() {
+        return mIsUserSetupComplete;
+    }
+
+    private void notifyUserUnlocked() {
+        for (Runnable action : mUserUnlockedActions) {
+            action.run();
+        }
+        mUserUnlockedActions.clear();
+        Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver);
+    }
+
+    /**
+     * @return whether the given running task info matches the gesture-blocked activity.
+     */
+    public boolean isGestureBlockedActivity(ActivityManager.RunningTaskInfo runningTaskInfo) {
+        return runningTaskInfo != null
+                && mGestureBlockedActivities.contains(runningTaskInfo.topActivity);
+    }
+
+    /**
+     * @return the packages of gesture-blocked activities.
+     */
+    public List<String> getGestureBlockedActivityPackages() {
+        return mGestureBlockedActivities.stream().map(ComponentName::getPackageName)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Updates the system ui state flags from SystemUI.
+     */
+    public void setSystemUiFlags(int stateFlags) {
+        mSystemUiStateFlags = stateFlags;
+    }
+
+    /**
+     * @return the system ui state flags.
+     */
+    // TODO(141886704): See if we can remove this
+    public @SystemUiStateFlags int getSystemUiStateFlags() {
+        return mSystemUiStateFlags;
+    }
+
+    /**
+     * @return whether SystemUI is in a state where we can start a system gesture.
+     */
+    public boolean canStartSystemGesture() {
+        boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
+                || mTaskListFrozen;
+        return canStartWithNavHidden
+                && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
+                && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
+                && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
+                        || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
+    }
+
+    /**
+     * @return whether the keyguard is showing and is occluded by an app showing above the keyguard
+     *         (like camera or maps)
+     */
+    public boolean isKeyguardShowingOccluded() {
+        return (mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0;
+    }
+
+    /**
+     * @return whether screen pinning is enabled and active
+     */
+    public boolean isScreenPinningActive() {
+        return (mSystemUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
+    }
+
+    /**
+     * @return whether the bubble stack is expanded
+     */
+    public boolean isBubblesExpanded() {
+        return (mSystemUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
+    }
+
+    /**
+     * @return whether the global actions dialog is showing
+     */
+    public boolean isGlobalActionsShowing() {
+        return (mSystemUiStateFlags & SYSUI_STATE_GLOBAL_ACTIONS_SHOWING) != 0;
+    }
+
+    /**
+     * @return whether lock-task mode is active
+     */
+    public boolean isLockToAppActive() {
+        return ActivityManagerWrapper.getInstance().isLockToAppActive();
+    }
+
+    /**
+     * @return whether the accessibility menu is available.
+     */
+    public boolean isAccessibilityMenuAvailable() {
+        return (mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
+    }
+
+    /**
+     * @return whether the accessibility menu shortcut is available.
+     */
+    public boolean isAccessibilityMenuShortcutAvailable() {
+        return (mSystemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
+    }
+
+    /**
+     * @return whether home is disabled (either by SUW/SysUI/device policy)
+     */
+    public boolean isHomeDisabled() {
+        return (mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0;
+    }
+
+    /**
+     * @return whether overview is disabled (either by SUW/SysUI/device policy)
+     */
+    public boolean isOverviewDisabled() {
+        return (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0;
+    }
+
+    /**
+     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
+     */
+    public void updateGestureTouchRegions() {
+        if (!mMode.hasGestures) {
+            return;
+        }
+
+        mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
+     *         is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
+                event.getY(pointerIndex));
+    }
+
+    /**
+     * Sets the region in screen space where the gestures should be deferred (ie. due to specific
+     * nav bar ui).
+     */
+    public void setDeferredGestureRegion(Region deferredGestureRegion) {
+        mDeferredGestureRegion.set(deferredGestureRegion);
+    }
+
+    /**
+     * @return whether the given {@param event} is in the deferred gesture region indicating that
+     *         the Launcher should not immediately start the recents animation until the gesture
+     *         passes a certain threshold.
+     */
+    public boolean isInDeferredGestureRegion(MotionEvent event) {
+        return mDeferredGestureRegion.contains((int) event.getX(), (int) event.getY());
+    }
+
+    /**
+     * @return whether the given {@param event} is in the app-requested gesture-exclusion region.
+     *         This is only used for quickswitch, and not swipe up.
+     */
+    public boolean isInExclusionRegion(MotionEvent event) {
+        // mExclusionRegion can change on binder thread, use a local instance here.
+        Region exclusionRegion = mExclusionRegion;
+        return mMode == NO_BUTTON && exclusionRegion != null
+                && exclusionRegion.contains((int) event.getX(), (int) event.getY());
+    }
+
+    /**
+     * Sets whether the assistant is available.
+     */
+    public void setAssistantAvailable(boolean assistantAvailable) {
+        mAssistantAvailable = assistantAvailable;
+    }
+
+    /**
+     * Sets the visibility fraction of the assistant.
+     */
+    public void setAssistantVisibility(float visibility) {
+        mAssistantVisibility = visibility;
+    }
+
+    /**
+     * @return the visibility fraction of the assistant.
+     */
+    public float getAssistantVisibility() {
+        return mAssistantVisibility;
+    }
+
+    /**
+     * @param ev An ACTION_DOWN motion event
+     * @param task Info for the currently running task
+     * @return whether the given motion event can trigger the assistant over the current task.
+     */
+    public boolean canTriggerAssistantAction(MotionEvent ev, ActivityManager.RunningTaskInfo task) {
+        return mAssistantAvailable
+                && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
+                && mOrientationTouchTransformer.touchInAssistantRegion(ev)
+                && !isLockToAppActive()
+                && !isGestureBlockedActivity(task);
+    }
+
+    /**
+     * *May* apply a transform on the motion event if it lies in the nav bar region for another
+     * orientation that is currently being tracked as a part of quickstep
+     */
+    void setOrientationTransformIfNeeded(MotionEvent event) {
+        // negative coordinates bug b/143901881
+        if (event.getX() < 0 || event.getY() < 0) {
+            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
+        }
+        mOrientationTouchTransformer.transform(event);
+    }
+
+    private void enableMultipleRegions(boolean enable) {
+        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
+        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
+            // Clear any previous state from sensor manager
+            mSensorRotation = mCurrentAppRotation;
+            mOrientationListener.enable();
+        } else {
+            mOrientationListener.disable();
+        }
+    }
+
+    public void onStartGesture() {
+        if (mTaskListFrozen) {
+            // Prioritize whatever nav bar user touches once in quickstep
+            // This case is specifically when user changes what nav bar they are using mid
+            // quickswitch session before tasks list is unfrozen
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
+            BaseActivityInterface activityInterface) {
+        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+            mInOverview = true;
+            if (!mTaskListFrozen) {
+                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
+                enableMultipleRegions(true);
+            }
+            activityInterface.onExitOverview(this, mExitOverviewRunnable);
+        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
+            enableMultipleRegions(false);
+        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
+            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
+                // First gesture to start quickswitch
+                enableMultipleRegions(true);
+            } else {
+                notifySysuiOfCurrentRotation(
+                        mOrientationTouchTransformer.getCurrentActiveRotation());
+            }
+
+            // A new gesture is starting, reset the current device rotation
+            // This is done under the assumption that the user won't rotate the phone and then
+            // quickswitch in the old orientation.
+            mPrioritizeDeviceRotation = false;
+        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
+            if (!mTaskListFrozen) {
+                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
+                return;
+            }
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    private void notifySysuiOfCurrentRotation(int rotation) {
+        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
+                .onQuickSwitchToNewTask(rotation));
+    }
+
+    /**
+     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
+     * notifies system UI of the primary rotation the user is interacting with
+     */
+    private void toggleSecondaryNavBarsForRotation() {
+        mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+    }
+
+    public int getCurrentActiveRotation() {
+        if (!mMode.hasGestures) {
+            // touch rotation should always match that of display for 3 button
+            return mDisplayRotation;
+        }
+        return mOrientationTouchTransformer.getCurrentActiveRotation();
+    }
+
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("DeviceState:");
+        pw.println("  canStartSystemGesture=" + canStartSystemGesture());
+        pw.println("  systemUiFlags=" + mSystemUiStateFlags);
+        pw.println("  systemUiFlagsDesc="
+                + QuickStepContract.getSystemUiStateString(mSystemUiStateFlags));
+        pw.println("  assistantAvailable=" + mAssistantAvailable);
+        pw.println("  assistantDisabled="
+                + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        pw.println("  displayRotation=" + getDisplayRotation());
+        pw.println("  isUserUnlocked=" + mIsUserUnlocked);
+        mOrientationTouchTransformer.dump(pw);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
new file mode 100644
index 0000000..718c5ba
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.graphics.Rect;
+
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/**
+ * Extension of {@link RemoteAnimationTargets} with additional information about swipe
+ * up animation
+ */
+public class RecentsAnimationTargets extends RemoteAnimationTargets {
+
+    public final Rect homeContentInsets;
+    public final Rect minimizedHomeBounds;
+
+    public RecentsAnimationTargets(RemoteAnimationTargetCompat[] apps,
+            RemoteAnimationTargetCompat[] wallpapers, Rect homeContentInsets,
+            Rect minimizedHomeBounds) {
+        super(apps, wallpapers, MODE_CLOSING);
+        this.homeContentInsets = homeContentInsets;
+        this.minimizedHomeBounds = minimizedHomeBounds;
+    }
+
+    public boolean hasTargets() {
+        return unfilteredApps.length != 0;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 2e59ed5..517501a 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -28,14 +28,10 @@
 import android.os.Build;
 import android.os.Looper;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.UserHandle;
-import android.util.Log;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.LauncherAppsCompat.OnAppsChangedCallbackCompat;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -52,17 +48,13 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel extends TaskStackChangeListener {
 
-    private static final String TAG = "RecentsModel";
-
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
             new MainThreadInitializedObject<>(RecentsModel::new);
 
-    private final List<TaskThumbnailChangeListener> mThumbnailChangeListeners = new ArrayList<>();
+    private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
     private final Context mContext;
 
-    private ISystemUiProxy mSystemUiProxy;
-
     private final RecentTasksList mTaskList;
     private final TaskIconCache mIconCache;
     private final TaskThumbnailCache mThumbnailCache;
@@ -75,8 +67,10 @@
                 new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
         mIconCache = new TaskIconCache(context, looper);
         mThumbnailCache = new TaskThumbnailCache(context, looper);
+
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
-        setupPackageListener();
+        IconProvider.registerIconChangeListener(context,
+                this::onPackageIconChanged, MAIN_EXECUTOR.getHandler());
     }
 
     public TaskIconCache getIconCache() {
@@ -178,14 +172,6 @@
         mIconCache.onTaskRemoved(dummyKey);
     }
 
-    public void setSystemUiProxy(ISystemUiProxy systemUiProxy) {
-        mSystemUiProxy = systemUiProxy;
-    }
-
-    public ISystemUiProxy getSystemUiProxy() {
-        return mSystemUiProxy;
-    }
-
     public void onTrimMemory(int level) {
         if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
             mThumbnailCache.getHighResLoadingState().setVisible(false);
@@ -197,44 +183,40 @@
         }
     }
 
-    public void onOverviewShown(boolean fromHome, String tag) {
-        if (mSystemUiProxy == null) {
-            return;
-        }
-        try {
-            mSystemUiProxy.onOverviewShown(fromHome);
-        } catch (RemoteException e) {
-            Log.w(tag,
-                    "Failed to notify SysUI of overview shown from " + (fromHome ? "home" : "app")
-                            + ": ", e);
+    private void onPackageIconChanged(String pkg, UserHandle user) {
+        mIconCache.invalidateCacheEntries(pkg, user);
+        for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
+            mThumbnailChangeListeners.get(i).onTaskIconChanged(pkg, user);
         }
     }
 
-    private void setupPackageListener() {
-        LauncherAppsCompat.getInstance(mContext)
-                .addOnAppsChangedCallback(new OnAppsChangedCallbackCompat() {
-                    @Override
-                    public void onPackageRemoved(String packageName, UserHandle user) {
-                        mIconCache.invalidatePackage(packageName);
-                    }
-
-                    @Override
-                    public void onPackageChanged(String packageName, UserHandle user) {
-                        mIconCache.invalidatePackage(packageName);
-                    }
-                });
-    }
-
-    public void addThumbnailChangeListener(TaskThumbnailChangeListener listener) {
+    /**
+     * Adds a listener for visuals changes
+     */
+    public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
 
-    public void removeThumbnailChangeListener(TaskThumbnailChangeListener listener) {
+    /**
+     * Removes a previously added listener
+     */
+    public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
 
-    public interface TaskThumbnailChangeListener {
+    /**
+     * Listener for receiving various task properties changes
+     */
+    public interface TaskVisualsChangeListener {
 
+        /**
+         * Called whn the task thumbnail changes
+         */
         Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData);
+
+        /**
+         * Called when the icon for a task changes
+         */
+        void onTaskIconChanged(String pkg, UserHandle user);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
new file mode 100644
index 0000000..ab5e3ba
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
@@ -0,0 +1,139 @@
+/*
+ * 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.quickstep;
+
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Holds a collection of RemoteAnimationTargets, filtered by different properties.
+ */
+public class RemoteAnimationTargets {
+
+    private final CopyOnWriteArrayList<ReleaseCheck> mReleaseChecks = new CopyOnWriteArrayList<>();
+
+    public final RemoteAnimationTargetCompat[] unfilteredApps;
+    public final RemoteAnimationTargetCompat[] apps;
+    public final RemoteAnimationTargetCompat[] wallpapers;
+    public final int targetMode;
+    public final boolean hasRecents;
+
+    private boolean mReleased = false;
+
+    public RemoteAnimationTargets(RemoteAnimationTargetCompat[] apps,
+            RemoteAnimationTargetCompat[] wallpapers, int targetMode) {
+        ArrayList<RemoteAnimationTargetCompat> filteredApps = new ArrayList<>();
+        boolean hasRecents = false;
+        if (apps != null) {
+            for (RemoteAnimationTargetCompat target : apps) {
+                if (target.mode == targetMode) {
+                    filteredApps.add(target);
+                }
+
+                hasRecents |= target.activityType ==
+                        RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
+            }
+        }
+
+        this.unfilteredApps = apps;
+        this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]);
+        this.wallpapers = wallpapers;
+        this.targetMode = targetMode;
+        this.hasRecents = hasRecents;
+    }
+
+    public RemoteAnimationTargetCompat findTask(int taskId) {
+        for (RemoteAnimationTargetCompat target : apps) {
+            if (target.taskId == taskId) {
+                return target;
+            }
+        }
+        return null;
+    }
+
+    public boolean isAnimatingHome() {
+        for (RemoteAnimationTargetCompat target : unfilteredApps) {
+            if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void addReleaseCheck(ReleaseCheck check) {
+        mReleaseChecks.add(check);
+    }
+
+    public void release() {
+        if (mReleased) {
+            return;
+        }
+        for (ReleaseCheck check : mReleaseChecks) {
+            if (!check.mCanRelease) {
+                check.addOnSafeToReleaseCallback(this::release);
+                return;
+            }
+        }
+        mReleaseChecks.clear();
+        mReleased = true;
+
+        for (RemoteAnimationTargetCompat target : unfilteredApps) {
+            target.release();
+        }
+        for (RemoteAnimationTargetCompat target : wallpapers) {
+            target.release();
+        }
+    }
+
+    /**
+     * Interface for intercepting surface release method
+     */
+    public static class ReleaseCheck {
+
+        boolean mCanRelease = false;
+        private Runnable mAfterApplyCallback;
+
+        protected void setCanRelease(boolean canRelease) {
+            mCanRelease = canRelease;
+            if (mCanRelease && mAfterApplyCallback != null) {
+                Runnable r = mAfterApplyCallback;
+                mAfterApplyCallback = null;
+                r.run();
+            }
+        }
+
+        /**
+         * Adds a callback to notify when the surface can safely be released
+         */
+        void addOnSafeToReleaseCallback(Runnable callback) {
+            if (mCanRelease) {
+                callback.run();
+            } else {
+                if (mAfterApplyCallback == null) {
+                    mAfterApplyCallback = callback;
+                } else {
+                    final Runnable oldCallback = mAfterApplyCallback;
+                    mAfterApplyCallback = () -> {
+                        callback.run();
+                        oldCallback.run();
+                    };
+                }
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index b67c6f8..05ce2a2 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -24,8 +24,10 @@
 import android.content.res.Resources;
 import android.util.Log;
 
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -52,12 +54,12 @@
         return INSTANCE.get(context).getMode();
     }
 
-    public static MainThreadInitializedObject<SysUINavigationMode> INSTANCE =
+    public static final MainThreadInitializedObject<SysUINavigationMode> INSTANCE =
             new MainThreadInitializedObject<>(SysUINavigationMode::new);
 
     private static final String TAG = "SysUINavigationMode";
 
-    private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
+    private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String NAV_BAR_INTERACTION_MODE_RES_NAME =
             "config_navBarInteractionMode";
 
@@ -73,15 +75,20 @@
         mContext.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
-                Mode oldMode = mMode;
-                initializeMode();
-                if (mMode != oldMode) {
-                    dispatchModeChange();
-                }
+                updateMode();
             }
         }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
     }
 
+    /** Updates navigation mode when needed. */
+    public void updateMode() {
+        Mode oldMode = mMode;
+        initializeMode();
+        if (mMode != oldMode) {
+            dispatchModeChange();
+        }
+    }
+
     private void initializeMode() {
         int modeInt = getSystemIntegerRes(mContext, NAV_BAR_INTERACTION_MODE_RES_NAME);
         for(Mode m : Mode.values()) {
@@ -122,6 +129,23 @@
         }
     }
 
+    /** @return Whether we can remove the shelf from overview. */
+    public static boolean removeShelfFromOverview(Context context) {
+        // The shelf is core to the two-button mode model, so we need to continue supporting it.
+        return getMode(context) != Mode.TWO_BUTTONS;
+    }
+
+    public static boolean hideShelfInTwoButtonLandscape(Context context,
+            PagedOrientationHandler pagedOrientationHandler) {
+        return  getMode(context) == Mode.TWO_BUTTONS &&
+                !pagedOrientationHandler.isLayoutNaturalToLauncher();
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("SysUINavigationMode:");
+        pw.println("  mode=" + mMode.name());
+    }
+
     public interface NavigationModeChangeListener {
 
         void onNavigationModeChanged(Mode newMode);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
new file mode 100644
index 0000000..299e9e5
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+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.ISystemUiProxy;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Holds the reference to SystemUI.
+ */
+public class SystemUiProxy implements ISystemUiProxy {
+    private static final String TAG = SystemUiProxy.class.getSimpleName();
+
+    public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
+            new MainThreadInitializedObject<>(SystemUiProxy::new);
+
+    private ISystemUiProxy mSystemUiProxy;
+    private final DeathRecipient mSystemUiProxyDeathRecipient = () -> {
+        MAIN_EXECUTOR.execute(() -> setProxy(null));
+    };
+
+    // Used to dedupe calls to SystemUI
+    private int mLastShelfHeight;
+    private boolean mLastShelfVisible;
+    private float mLastBackButtonAlpha;
+    private boolean mLastBackButtonAnimate;
+
+    // TODO(141886704): Find a way to remove this
+    private int mLastSystemUiStateFlags;
+
+    public SystemUiProxy(Context context) {
+        // Do nothing
+    }
+
+    @Override
+    public IBinder asBinder() {
+        // Do nothing
+        return null;
+    }
+
+    public void setProxy(ISystemUiProxy proxy) {
+        unlinkToDeath();
+        mSystemUiProxy = proxy;
+        linkToDeath();
+    }
+
+    // TODO(141886704): Find a way to remove this
+    public void setLastSystemUiStateFlags(int stateFlags) {
+        mLastSystemUiStateFlags = stateFlags;
+    }
+
+    // TODO(141886704): Find a way to remove this
+    public int getLastSystemUiStateFlags() {
+        return mLastSystemUiStateFlags;
+    }
+
+    public boolean isActive() {
+        return mSystemUiProxy != null;
+    }
+
+    private void linkToDeath() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.asBinder().linkToDeath(mSystemUiProxyDeathRecipient, 0 /* flags */);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to link sysui proxy death recipient");
+            }
+        }
+    }
+
+    private void unlinkToDeath() {
+        if (mSystemUiProxy != null) {
+            mSystemUiProxy.asBinder().unlinkToDeath(mSystemUiProxyDeathRecipient, 0 /* flags */);
+        }
+    }
+
+    @Override
+    public void startScreenPinning(int taskId) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startScreenPinning(taskId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startScreenPinning", e);
+            }
+        }
+    }
+
+    @Override
+    public void onSplitScreenInvoked() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onSplitScreenInvoked();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onSplitScreenInvoked", e);
+            }
+        }
+    }
+
+    @Override
+    public void onOverviewShown(boolean fromHome) {
+        onOverviewShown(fromHome, TAG);
+    }
+
+    public void onOverviewShown(boolean fromHome, String tag) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onOverviewShown(fromHome);
+            } catch (RemoteException e) {
+                Log.w(tag, "Failed call onOverviewShown from: " + (fromHome ? "home" : "app"), e);
+            }
+        }
+    }
+
+    @Override
+    public Rect getNonMinimizedSplitScreenSecondaryBounds() {
+        if (mSystemUiProxy != null) {
+            try {
+                return mSystemUiProxy.getNonMinimizedSplitScreenSecondaryBounds();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call getNonMinimizedSplitScreenSecondaryBounds", e);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void setBackButtonAlpha(float alpha, boolean animate) {
+        boolean changed = Float.compare(alpha, mLastBackButtonAlpha) != 0
+                || animate != mLastBackButtonAnimate;
+        if (mSystemUiProxy != null && changed) {
+            mLastBackButtonAlpha = alpha;
+            mLastBackButtonAnimate = animate;
+            try {
+                mSystemUiProxy.setBackButtonAlpha(alpha, animate);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setBackButtonAlpha", e);
+            }
+        }
+    }
+
+    public float getLastBackButtonAlpha() {
+        return mLastBackButtonAlpha;
+    }
+
+    @Override
+    public void setNavBarButtonAlpha(float alpha, boolean animate) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setNavBarButtonAlpha", e);
+            }
+        }
+    }
+
+    @Override
+    public void onStatusBarMotionEvent(MotionEvent event) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onStatusBarMotionEvent(event);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onStatusBarMotionEvent", e);
+            }
+        }
+    }
+
+    @Override
+    public void onAssistantProgress(float progress) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onAssistantProgress(progress);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onAssistantProgress with progress: " + progress, e);
+            }
+        }
+    }
+
+    @Override
+    public void onAssistantGestureCompletion(float velocity) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onAssistantGestureCompletion(velocity);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onAssistantGestureCompletion", e);
+            }
+        }
+    }
+
+    @Override
+    public void startAssistant(Bundle args) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startAssistant(args);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startAssistant", e);
+            }
+        }
+    }
+
+    @Override
+    public Bundle monitorGestureInput(String name, int displayId) {
+        if (mSystemUiProxy != null) {
+            try {
+                return mSystemUiProxy.monitorGestureInput(name, displayId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call monitorGestureInput: " + name, e);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void notifyAccessibilityButtonClicked(int displayId) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.notifyAccessibilityButtonClicked(displayId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call notifyAccessibilityButtonClicked", e);
+            }
+        }
+    }
+
+    @Override
+    public void notifyAccessibilityButtonLongClicked() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.notifyAccessibilityButtonLongClicked();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call notifyAccessibilityButtonLongClicked", e);
+            }
+        }
+    }
+
+    @Override
+    public void stopScreenPinning() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.stopScreenPinning();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stopScreenPinning", e);
+            }
+        }
+    }
+
+    @Override
+    public void setShelfHeight(boolean visible, int shelfHeight) {
+        boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight;
+        if (mSystemUiProxy != null && changed) {
+            mLastShelfVisible = visible;
+            mLastShelfHeight = shelfHeight;
+            try {
+                mSystemUiProxy.setShelfHeight(visible, shelfHeight);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setShelfHeight visible: " + visible
+                        + " height: " + shelfHeight, e);
+            }
+        }
+    }
+
+    @Override
+    public void handleImageAsScreenshot(Bitmap bitmap, Rect rect, Insets insets, int i) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.handleImageAsScreenshot(bitmap, rect, insets, i);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call handleImageAsScreenshot", e);
+            }
+        }
+    }
+
+    @Override
+    public void setSplitScreenMinimized(boolean minimized) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setSplitScreenMinimized(minimized);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stopScreenPinning", e);
+            }
+        }
+    }
+
+    /**
+     * Notifies that swipe-to-home action is finished.
+     */
+    @Override
+    public void notifySwipeToHomeFinished() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.notifySwipeToHomeFinished();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setPinnedStackAnimationType", e);
+            }
+        }
+    }
+
+    /**
+     * Sets listener to get pinned stack animation callbacks.
+     */
+    @Override
+    public void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setPinnedStackAnimationListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
+            }
+        }
+    }
+
+    @Override
+    public void onQuickSwitchToNewTask(int rotation) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onQuickSwitchToNewTask(rotation);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onQuickSwitchToNewTask with arg: " + rotation, e);
+            }
+        }
+    }
+
+    @Override
+    public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
+            Insets visibleInsets, Task.TaskKey task) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen,
+                        visibleInsets, task);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call handleImageBundleAsScreenshot");
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
new file mode 100644
index 0000000..cad51f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
+import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
+
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
+
+    private RecentsAnimationController mController;
+    private RecentsAnimationCallbacks mCallbacks;
+    private RecentsAnimationTargets mTargets;
+    // Temporary until we can hook into gesture state events
+    private GestureState mLastGestureState;
+    private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
+
+    /**
+     * Preloads the recents animation.
+     */
+    public void preloadRecentsAnimation(Intent intent) {
+        // Pass null animation handler to indicate this start is for preloading
+        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+                .startRecentsActivity(intent, null, null, null, null));
+    }
+
+    /**
+     * Starts a new recents animation for the activity with the given {@param intent}.
+     */
+    @UiThread
+    public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
+            Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
+        // Notify if recents animation is still running
+        if (mController != null) {
+            String msg = "New recents animation started before old animation completed";
+            if (FeatureFlags.IS_STUDIO_BUILD) {
+                throw new IllegalArgumentException(msg);
+            } else {
+                Log.e("TaskAnimationManager", msg, new Exception());
+            }
+        }
+        // But force-finish it anyways
+        finishRunningRecentsAnimation(false /* toHome */);
+
+        final BaseActivityInterface activityInterface = gestureState.getActivityInterface();
+        mLastGestureState = gestureState;
+        mCallbacks = new RecentsAnimationCallbacks(activityInterface.allowMinimizeSplitScreen());
+        mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+            @Override
+            public void onRecentsAnimationStart(RecentsAnimationController controller,
+                    RecentsAnimationTargets targets) {
+                if (mCallbacks == null) {
+                    // It's possible for the recents animation to have finished and be cleaned up
+                    // by the time we process the start callback, and in that case, just we can skip
+                    // handling this call entirely
+                    return;
+                }
+                mController = controller;
+                mTargets = targets;
+                mLastAppearedTaskTarget = mTargets.findTask(mLastGestureState.getRunningTaskId());
+                mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+            }
+
+            @Override
+            public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+                if (thumbnailData != null) {
+                    // If a screenshot is provided, switch to the screenshot before cleaning up
+                    activityInterface.switchRunningTaskViewToScreenshot(thumbnailData,
+                            () -> cleanUpRecentsAnimation(thumbnailData));
+                } else {
+                    cleanUpRecentsAnimation(null /* canceledThumbnail */);
+                }
+            }
+
+            @Override
+            public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+                cleanUpRecentsAnimation(null /* canceledThumbnail */);
+            }
+
+            @Override
+            public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+                if (mController != null) {
+                    if (mLastAppearedTaskTarget == null
+                            || appearedTaskTarget.taskId != mLastAppearedTaskTarget.taskId) {
+                        if (mLastAppearedTaskTarget != null) {
+                            mController.removeTaskTarget(mLastAppearedTaskTarget);
+                        }
+                        mLastAppearedTaskTarget = appearedTaskTarget;
+                        mLastGestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+                    }
+                }
+            }
+        });
+        mCallbacks.addListener(gestureState);
+        mCallbacks.addListener(listener);
+        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+                .startRecentsActivity(intent, null, mCallbacks, null, null));
+        gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
+        return mCallbacks;
+    }
+
+    /**
+     * Continues the existing running recents animation for a new gesture.
+     */
+    public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) {
+        mCallbacks.removeListener(mLastGestureState);
+        mLastGestureState = gestureState;
+        mCallbacks.addListener(gestureState);
+        gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED
+                | STATE_RECENTS_ANIMATION_STARTED);
+        gestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
+        return mCallbacks;
+    }
+
+    /**
+     * Finishes the running recents animation.
+     */
+    public void finishRunningRecentsAnimation(boolean toHome) {
+        if (mController != null) {
+            mCallbacks.notifyAnimationCanceled();
+            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
+                    ? mController::finishAnimationToHome
+                    : mController::finishAnimationToApp);
+            cleanUpRecentsAnimation(null /* canceledThumbnail */);
+        }
+    }
+
+    /**
+     * Used to notify a listener of the current recents animation state (used if the listener was
+     * not yet added to the callbacks at the point that the listener callbacks would have been
+     * made).
+     */
+    public void notifyRecentsAnimationState(
+            RecentsAnimationCallbacks.RecentsAnimationListener listener) {
+        if (isRecentsAnimationRunning()) {
+            listener.onRecentsAnimationStart(mController, mTargets);
+        }
+        // TODO: Do we actually need to report canceled/finished?
+    }
+
+    /**
+     * @return whether there is a recents animation running.
+     */
+    public boolean isRecentsAnimationRunning() {
+        return mController != null;
+    }
+
+    /**
+     * Cleans up the recents animation entirely.
+     */
+    private void cleanUpRecentsAnimation(ThumbnailData canceledThumbnail) {
+        // Clean up the screenshot if necessary
+        if (mController != null && canceledThumbnail != null) {
+            mController.cleanupScreenshot();
+        }
+
+        // Release all the target leashes
+        if (mTargets != null) {
+            mTargets.release();
+        }
+
+        // Clean up all listeners to ensure we don't get subsequent callbacks
+        if (mCallbacks != null) {
+            mCallbacks.removeAllListeners();
+        }
+
+        mController = null;
+        mCallbacks = null;
+        mTargets = null;
+        mLastGestureState = null;
+        mLastAppearedTaskTarget = null;
+    }
+
+    public void dump() {
+        // TODO
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 289a129..7ff799e 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,67 +15,65 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.uioverrides.RecentsUiFactory.GO_LOW_RAM_RECENTS_ENABLED;
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.content.ComponentName;
+import android.app.ActivityManager.TaskDescription;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.LruCache;
+import android.os.UserHandle;
+import android.util.SparseArray;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.shared.system.TaskDescriptionCompat;
 
-import java.util.Map;
 import java.util.function.Consumer;
 
 /**
  * Manages the caching of task icons and related data.
- * TODO(b/138944598): This class should later be merged into IconCache.
  */
 public class TaskIconCache {
 
     private final Handler mBackgroundHandler;
     private final AccessibilityManager mAccessibilityManager;
 
-    private final NormalizedIconLoader mIconLoader;
-
-    private final TaskKeyLruCache<Drawable> mIconCache;
-    private final TaskKeyLruCache<String> mContentDescriptionCache;
-    private final LruCache<ComponentName, ActivityInfo> mActivityInfoCache;
-
-    private TaskKeyLruCache.EvictionCallback mClearActivityInfoOnEviction =
-            new TaskKeyLruCache.EvictionCallback() {
-        @Override
-        public void onEntryEvicted(Task.TaskKey key) {
-            if (key != null) {
-                mActivityInfoCache.remove(key.getComponent());
-            }
-        }
-    };
+    private final Context mContext;
+    private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
+    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
+    private final IconProvider mIconProvider;
 
     public TaskIconCache(Context context, Looper backgroundLooper) {
+        mContext = context;
         mBackgroundHandler = new Handler(backgroundLooper);
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
 
         Resources res = context.getResources();
         int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
-        mIconCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
-        mContentDescriptionCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
-        mActivityInfoCache = new LruCache<>(cacheSize);
-        mIconLoader = new NormalizedIconLoader(context, mIconCache, mActivityInfoCache,
-                true /* disableColorExtraction */);
+        mIconCache = new TaskKeyLruCache<>(cacheSize);
+        mIconProvider = new IconProvider(context);
     }
 
     /**
@@ -96,15 +94,14 @@
         IconLoadRequest request = new IconLoadRequest(mBackgroundHandler) {
             @Override
             public void run() {
-                Drawable icon = mIconLoader.getIcon(task);
-                String contentDescription = loadContentDescriptionInBackground(task);
+                TaskCacheEntry entry = getCacheEntry(task);
                 if (isCanceled()) {
                     // We don't call back to the provided callback in this case
                     return;
                 }
                 MAIN_EXECUTOR.execute(() -> {
-                    task.icon = icon;
-                    task.titleDescription = contentDescription;
+                    task.icon = entry.icon;
+                    task.titleDescription = entry.contentDescription;
                     callback.accept(task);
                     onEnd();
                 });
@@ -116,51 +113,99 @@
 
     public void clear() {
         mIconCache.evictAll();
-        mContentDescriptionCache.evictAll();
     }
 
-    /**
-     * Loads the content description for the given {@param task}.
-     */
-    private String loadContentDescriptionInBackground(Task task) {
-        // Return the cached content description if it exists
-        String label = mContentDescriptionCache.getAndInvalidateIfModified(task.key);
-        if (label != null) {
-            return label;
-        }
-
-        // Skip loading content descriptions if accessibility is disabled unless low RAM recents
-        // is enabled.
-        if (!GO_LOW_RAM_RECENTS_ENABLED && !mAccessibilityManager.isEnabled()) {
-            return "";
-        }
-
-        // Skip loading the content description if the activity no longer exists
-        ActivityInfo activityInfo = mIconLoader.getAndUpdateActivityInfo(task.key);
-        if (activityInfo == null) {
-            return "";
-        }
-
-        // Load the label otherwise
-        label = ActivityManagerWrapper.getInstance().getBadgedContentDescription(activityInfo,
-                task.key.userId, task.taskDescription);
-        mContentDescriptionCache.put(task.key, label);
-        return label;
-    }
-
-
     void onTaskRemoved(TaskKey taskKey) {
         mIconCache.remove(taskKey);
     }
 
-    void invalidatePackage(String packageName) {
-        // TODO(b/138944598): Merge this class into IconCache so we can do this at the base level
-        Map<ComponentName, ActivityInfo> activityInfoCache = mActivityInfoCache.snapshot();
-        for (ComponentName cn : activityInfoCache.keySet()) {
-            if (cn.getPackageName().equals(packageName)) {
-                mActivityInfoCache.remove(cn);
+    void invalidateCacheEntries(String pkg, UserHandle handle) {
+        Utilities.postAsyncCallback(mBackgroundHandler,
+                () -> mIconCache.removeAll(key ->
+                        pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
+    }
+
+    @WorkerThread
+    private TaskCacheEntry getCacheEntry(Task task) {
+        TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
+        if (entry != null) {
+            return entry;
+        }
+
+        TaskDescription desc = task.taskDescription;
+        TaskKey key = task.key;
+        ActivityInfo activityInfo = null;
+
+        // Create new cache entry
+        entry = new TaskCacheEntry();
+
+        // Load icon
+        // TODO: Load icon resource (b/143363444)
+        Bitmap icon = TaskDescriptionCompat.getIcon(desc, key.userId);
+        if (icon != null) {
+            entry.icon = new FastBitmapDrawable(getBitmapInfo(
+                    new BitmapDrawable(mContext.getResources(), icon),
+                    key.userId,
+                    desc.getPrimaryColor(),
+                    false /* isInstantApp */));
+        } else {
+            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+                    key.getComponent(), key.userId);
+            if (activityInfo != null) {
+                BitmapInfo bitmapInfo = getBitmapInfo(
+                        mIconProvider.getIcon(activityInfo, UserHandle.of(key.userId)),
+                        key.userId,
+                        desc.getPrimaryColor(),
+                        activityInfo.applicationInfo.isInstantApp());
+                entry.icon = newIcon(mContext, bitmapInfo);
+            } else {
+                entry.icon = getDefaultIcon(key.userId);
             }
         }
+
+        // Loading content descriptions if accessibility or low RAM recents is enabled.
+        if (GO_LOW_RAM_RECENTS_ENABLED || mAccessibilityManager.isEnabled()) {
+            // Skip loading the content description if the activity no longer exists
+            if (activityInfo == null) {
+                activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+                        key.getComponent(), key.userId);
+            }
+            if (activityInfo != null) {
+                entry.contentDescription = ActivityManagerWrapper.getInstance()
+                        .getBadgedContentDescription(activityInfo, task.key.userId,
+                                task.taskDescription);
+            }
+        }
+
+        mIconCache.put(task.key, entry);
+        return entry;
+    }
+
+    @WorkerThread
+    private Drawable getDefaultIcon(int userId) {
+        synchronized (mDefaultIcons) {
+            BitmapInfo info = mDefaultIcons.get(userId);
+            if (info == null) {
+                try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
+                    info = la.makeDefaultIcon(UserHandle.of(userId));
+                }
+                mDefaultIcons.put(userId, info);
+            }
+            return new FastBitmapDrawable(info);
+        }
+    }
+
+    @WorkerThread
+    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
+            int primaryColor, boolean isInstantApp) {
+        try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
+            la.disableColorExtraction();
+            la.setWrapperBackgroundColor(primaryColor);
+
+            // User version code O, so that the icon is always wrapped in an adaptive icon container
+            return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
+                    Build.VERSION_CODES.O, isInstantApp);
+        }
     }
 
     public static abstract class IconLoadRequest extends HandlerRunnable {
@@ -168,4 +213,9 @@
             super(handler, null);
         }
     }
+
+    private static class TaskCacheEntry {
+        public Drawable icon;
+        public String contentDescription = "";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3b50c26..2b7a8ec 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Handler;
@@ -27,9 +26,9 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -41,11 +40,12 @@
     private final Handler mBackgroundHandler;
 
     private final int mCacheSize;
-    private final ThumbnailCache mCache;
+    private final TaskKeyLruCache<ThumbnailData> mCache;
     private final HighResLoadingState mHighResLoadingState;
+    private final boolean mEnableTaskSnapshotPreloading;
 
     public static class HighResLoadingState {
-        private boolean mIsLowRamDevice;
+        private boolean mForceHighResThumbnails;
         private boolean mVisible;
         private boolean mFlingingFast;
         private boolean mHighResLoadingEnabled;
@@ -56,9 +56,9 @@
         }
 
         private HighResLoadingState(Context context) {
-            ActivityManager activityManager =
-                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-            mIsLowRamDevice = activityManager.isLowRamDevice();
+            // If the device does not support low-res thumbnails, only attempt to load high-res
+            // thumbnails
+            mForceHighResThumbnails = !supportsLowResThumbnails();
         }
 
         public void addCallback(HighResLoadingStateChangedCallback callback) {
@@ -85,7 +85,7 @@
 
         private void updateState() {
             boolean prevState = mHighResLoadingEnabled;
-            mHighResLoadingEnabled = !mIsLowRamDevice && mVisible && !mFlingingFast;
+            mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
             if (prevState != mHighResLoadingEnabled) {
                 for (int i = mCallbacks.size() - 1; i >= 0; i--) {
                     mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
@@ -100,7 +100,8 @@
 
         Resources res = context.getResources();
         mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
-        mCache = new ThumbnailCache(mCacheSize);
+        mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
+        mCache = new TaskKeyLruCache<>(mCacheSize);
     }
 
     /**
@@ -110,7 +111,7 @@
         Preconditions.assertUIThread();
         // Fetch the thumbnail for this task and put it in the cache
         if (task.thumbnail == null) {
-            updateThumbnailInBackground(task.key, true /* reducedResolution */,
+            updateThumbnailInBackground(task.key, true /* lowResolution */,
                     t -> task.thumbnail = t);
         }
     }
@@ -133,8 +134,8 @@
             Task task, Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
 
-        boolean reducedResolution = !mHighResLoadingState.isEnabled();
-        if (task.thumbnail != null && (!task.thumbnail.reducedResolution || reducedResolution)) {
+        boolean lowResolution = !mHighResLoadingState.isEnabled();
+        if (task.thumbnail != null && (!task.thumbnail.reducedResolution || lowResolution)) {
             // Nothing to load, the thumbnail is already high-resolution or matches what the
             // request, so just callback
             callback.accept(task.thumbnail);
@@ -148,28 +149,30 @@
         });
     }
 
-    private ThumbnailLoadRequest updateThumbnailInBackground(TaskKey key, boolean reducedResolution,
+    private ThumbnailLoadRequest updateThumbnailInBackground(TaskKey key, boolean lowResolution,
             Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
 
         ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-        if (cachedThumbnail != null && (!cachedThumbnail.reducedResolution || reducedResolution)) {
+        if (cachedThumbnail != null && (!cachedThumbnail.reducedResolution || lowResolution)) {
             // Already cached, lets use that thumbnail
             callback.accept(cachedThumbnail);
             return null;
         }
 
         ThumbnailLoadRequest request = new ThumbnailLoadRequest(mBackgroundHandler,
-                reducedResolution) {
+                lowResolution) {
             @Override
             public void run() {
                 ThumbnailData thumbnail = ActivityManagerWrapper.getInstance().getTaskThumbnail(
-                        key.id, reducedResolution);
-                if (isCanceled()) {
-                    // We don't call back to the provided callback in this case
-                    return;
-                }
+                        key.id, lowResolution);
+
                 MAIN_EXECUTOR.execute(() -> {
+                    if (isCanceled()) {
+                        // We don't call back to the provided callback in this case
+                        return;
+                    }
+
                     mCache.put(key, thumbnail);
                     callback.accept(thumbnail);
                     onEnd();
@@ -212,32 +215,31 @@
      * @return Whether to enable background preloading of task thumbnails.
      */
     public boolean isPreloadingEnabled() {
-        return !mHighResLoadingState.mIsLowRamDevice && mHighResLoadingState.mVisible;
+        return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
     }
 
     public static abstract class ThumbnailLoadRequest extends HandlerRunnable {
-        public final boolean reducedResolution;
+        public final boolean mLowResolution;
 
-        ThumbnailLoadRequest(Handler handler, boolean reducedResolution) {
+        ThumbnailLoadRequest(Handler handler, boolean lowResolution) {
             super(handler, null);
-            this.reducedResolution = reducedResolution;
+            mLowResolution = lowResolution;
         }
     }
 
-    private static class ThumbnailCache extends TaskKeyLruCache<ThumbnailData> {
-
-        public ThumbnailCache(int cacheSize) {
-            super(cacheSize);
+    /**
+     * @return Whether device supports low-res thumbnails. Low-res files are an optimization
+     * for faster load times of snapshots. Devices can optionally disable low-res files so that
+     * they only store snapshots at high-res scale. The actual scale can be configured in
+     * frameworks/base config overlay.
+     */
+    private static boolean supportsLowResThumbnails() {
+        Resources res = Resources.getSystem();
+        int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
+        if (resId != 0) {
+            return 0 < res.getFloat(resId);
         }
-
-        /**
-         * Updates the cache entry if it is already present in the cache
-         */
-        public void updateIfAlreadyInCache(int taskId, ThumbnailData thumbnailData) {
-            ThumbnailData oldData = getCacheEntry(taskId);
-            if (oldData != null) {
-                putCacheEntry(taskId, thumbnailData);
-            }
-        }
+        return true;
     }
+
 }
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 5f76ca7..04b488d 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -23,9 +23,9 @@
 import android.os.UserHandle;
 import android.util.Log;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -44,15 +44,14 @@
      * TODO: remove this once we switch to getting the icon and label from IconCache.
      */
     public static CharSequence getTitle(Context context, Task task) {
-        LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context);
-        PackageManager packageManager = context.getPackageManager();
         UserHandle user = UserHandle.of(task.key.userId);
-        ApplicationInfo applicationInfo = launcherAppsCompat.getApplicationInfo(
-            task.getTopComponent().getPackageName(), 0, user);
+        ApplicationInfo applicationInfo = new PackageManagerHelper(context)
+                .getApplicationInfo(task.getTopComponent().getPackageName(), user, 0);
         if (applicationInfo == null) {
             Log.e(TAG, "Failed to get title for task " + task);
             return "";
         }
+        PackageManager packageManager = context.getPackageManager();
         return packageManager.getUserBadgedLabel(
             applicationInfo.loadLabel(packageManager), user);
     }
@@ -79,7 +78,7 @@
         if (currentUserId == UserHandle.myUserId()) {
             return true;
         }
-        List<UserHandle> allUsers = UserManagerCompat.getInstance(context).getUserProfiles();
+        List<UserHandle> allUsers = UserCache.INSTANCE.get(context).getUserProfiles();
         for (int i = allUsers.size() - 1; i >= 0; i--) {
             if (currentUserId == allUsers.get(i).getIdentifier()) {
                 return true;
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
new file mode 100644
index 0000000..6862f07
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialController.TutorialType.ASSISTANT_COMPLETE;
+
+import android.graphics.PointF;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Assistant tutorial. */
+final class AssistantGestureTutorialController extends TutorialController {
+
+    AssistantGestureTutorialController(AssistantGestureTutorialFragment fragment,
+                                       TutorialType tutorialType) {
+        super(fragment, tutorialType);
+    }
+
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                return R.string.assistant_gesture_tutorial_playground_title;
+            case ASSISTANT_COMPLETE:
+                return R.string.gesture_tutorial_confirm_title;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getSubtitleStringId() {
+        if (mTutorialType == TutorialType.ASSISTANT) {
+            return R.string.assistant_gesture_tutorial_playground_subtitle;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == ASSISTANT_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_done;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                switch (result) {
+                    case BACK_COMPLETED_FROM_LEFT:
+                    case BACK_COMPLETED_FROM_RIGHT:
+                    case BACK_CANCELLED_FROM_LEFT:
+                    case BACK_CANCELLED_FROM_RIGHT:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
+                        break;
+                }
+                break;
+            case ASSISTANT_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                switch (result) {
+                    case HOME_GESTURE_COMPLETED:
+                    case OVERVIEW_GESTURE_COMPLETED:
+                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+                    case HOME_OR_OVERVIEW_CANCELLED:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
+                        break;
+                    case ASSISTANT_COMPLETED:
+                        hideFeedback();
+                        hideHandCoachingAnimation();
+                        showRippleEffect(
+                                () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE));
+                        break;
+                    case ASSISTANT_NOT_STARTED_BAD_ANGLE:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal);
+                        break;
+                    case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_long_enough);
+                        break;
+                }
+                break;
+            case ASSISTANT_COMPLETE:
+                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void setAssistantProgress(float progress) {
+        // TODO: Create an animation.
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
new file mode 100644
index 0000000..70181fb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the Home gesture interactive tutorial. */
+public class AssistantGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.assistant_gesture;
+    }
+
+    @Override
+    TutorialController createController(TutorialType type) {
+        return new AssistantGestureTutorialController(this, type);
+    }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return AssistantGestureTutorialController.class;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
+        }
+        return super.onTouch(view, motionEvent);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
new file mode 100644
index 0000000..921e568
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
+import static com.android.quickstep.interaction.TutorialController.TutorialType.LEFT_EDGE_BACK_NAVIGATION;
+
+import android.graphics.PointF;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Back tutorial. */
+final class BackGestureTutorialController extends TutorialController {
+
+    BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
+        super(fragment, tutorialType);
+    }
+
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge;
+            case BACK_NAVIGATION_COMPLETE:
+                return R.string.gesture_tutorial_confirm_title;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getSubtitleStringId() {
+        switch (mTutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge;
+            case BACK_NAVIGATION_COMPLETE:
+                return R.string.back_gesture_tutorial_confirm_subtitle;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_done;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionTextButtonStringId() {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_settings;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    void onActionTextButtonClicked(View button) {
+        mTutorialFragment.startSystemNavigationSetting();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                handleAttemptFromRight(result);
+                break;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                handleAttemptFromLeft(result);
+                break;
+            case BACK_NAVIGATION_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+    private void handleAttemptFromRight(BackGestureResult result) {
+        switch (result) {
+            case BACK_COMPLETED_FROM_RIGHT:
+                hideFeedback();
+                hideHandCoachingAnimation();
+                showRippleEffect(
+                        () -> mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION));
+                break;
+            case BACK_CANCELLED_FROM_RIGHT:
+                showFeedback(R.string.back_gesture_feedback_cancelled_right_edge);
+                break;
+            case BACK_COMPLETED_FROM_LEFT:
+            case BACK_CANCELLED_FROM_LEFT:
+            case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_right_edge);
+                break;
+            case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
+                showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
+                break;
+        }
+    }
+
+    private void handleAttemptFromLeft(BackGestureResult result) {
+        switch (result) {
+            case BACK_COMPLETED_FROM_LEFT:
+                hideFeedback();
+                hideHandCoachingAnimation();
+                showRippleEffect(
+                        () -> mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE));
+                break;
+            case BACK_CANCELLED_FROM_LEFT:
+                showFeedback(R.string.back_gesture_feedback_cancelled_left_edge);
+                break;
+            case BACK_COMPLETED_FROM_RIGHT:
+            case BACK_CANCELLED_FROM_RIGHT:
+            case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_left_edge);
+                break;
+            case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
+                showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
+                break;
+        }
+    }
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                mTutorialFragment.closeTutorial();
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
new file mode 100644
index 0000000..bef50ea
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the Back gesture interactive tutorial. */
+public class BackGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.back_gesture;
+    }
+
+    @Override
+    TutorialController createController(TutorialType type) {
+        return new BackGestureTutorialController(this, type);
+    }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return BackGestureTutorialController.class;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
+        }
+        return super.onTouch(view, motionEvent);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
new file mode 100644
index 0000000..e4b348e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.os.SystemProperties;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.ResourceUtils;
+
+/**
+ * Utility class to handle edge swipes for back gestures.
+ *
+ * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
+ */
+public class EdgeBackGestureHandler implements OnTouchListener {
+
+    private static final String TAG = "EdgeBackGestureHandler";
+    private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
+            "gestures.back_timeout", 250);
+
+    private final Context mContext;
+
+    private final Point mDisplaySize = new Point();
+
+    // The edge width where touch down is allowed
+    private int mEdgeWidth;
+    // The bottom gesture area height
+    private int mBottomGestureHeight;
+    // The slop to distinguish between horizontal and vertical motion
+    private final float mTouchSlop;
+    // Duration after which we consider the event as longpress.
+    private final int mLongPressTimeout;
+
+    private final PointF mDownPoint = new PointF();
+    private boolean mThresholdCrossed = false;
+    private boolean mAllowGesture = false;
+    private BackGestureResult mDisallowedGestureReason;
+    private boolean mIsEnabled;
+    private int mLeftInset;
+    private int mRightInset;
+
+    private EdgeBackGesturePanel mEdgeBackPanel;
+    private BackGestureAttemptCallback mGestureCallback;
+
+    private final EdgeBackGesturePanel.BackCallback mBackCallback =
+            new EdgeBackGesturePanel.BackCallback() {
+                @Override
+                public void triggerBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                                : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
+                    }
+                }
+
+                @Override
+                public void cancelBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
+                                : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
+                    }
+                }
+            };
+
+    EdgeBackGestureHandler(Context context) {
+        final Resources res = context.getResources();
+        mContext = context;
+
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
+                ViewConfiguration.getLongPressTimeout());
+
+        mBottomGestureHeight =
+            ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
+        mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+    }
+
+    void setViewGroupParent(@Nullable ViewGroup parent) {
+        mIsEnabled = parent != null;
+
+        if (mEdgeBackPanel != null) {
+            mEdgeBackPanel.onDestroy();
+            mEdgeBackPanel = null;
+        }
+
+        if (mIsEnabled) {
+            // Add a nav bar panel window.
+            mEdgeBackPanel = new EdgeBackGesturePanel(mContext, parent, createLayoutParams());
+            mEdgeBackPanel.setBackCallback(mBackCallback);
+            if (mContext.getDisplay() != null) {
+                mContext.getDisplay().getRealSize(mDisplaySize);
+                mEdgeBackPanel.setDisplaySize(mDisplaySize);
+            }
+        }
+    }
+
+    void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
+        mGestureCallback = callback;
+    }
+
+    void unregisterBackGestureAttemptCallback() {
+        mGestureCallback = null;
+    }
+
+    private LayoutParams createLayoutParams() {
+        Resources resources = mContext.getResources();
+        return new LayoutParams(
+                ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
+                ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources));
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (mIsEnabled) {
+            onMotionEvent(motionEvent);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isWithinTouchRegion(int x, int y) {
+        // Disallow if too far from the edge
+        if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
+            mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_TOO_FAR_FROM_EDGE;
+            return false;
+        }
+
+        // Disallow if we are in the bottom gesture area
+        if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
+            mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_IN_NAV_BAR_REGION;
+            return false;
+        }
+
+        return true;
+    }
+
+    private void cancelGesture(MotionEvent ev) {
+        // Send action cancel to reset all the touch events
+        mAllowGesture = false;
+        MotionEvent cancelEv = MotionEvent.obtain(ev);
+        cancelEv.setAction(MotionEvent.ACTION_CANCEL);
+        mEdgeBackPanel.onMotionEvent(cancelEv);
+        cancelEv.recycle();
+    }
+
+    private void onMotionEvent(MotionEvent ev) {
+        int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
+            mDisallowedGestureReason = BackGestureResult.UNKNOWN;
+            mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+            mDownPoint.set(ev.getX(), ev.getY());
+            if (mAllowGesture) {
+                mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
+                mEdgeBackPanel.onMotionEvent(ev);
+                mThresholdCrossed = false;
+            }
+        } else if (mAllowGesture) {
+            if (!mThresholdCrossed) {
+                if (action == MotionEvent.ACTION_POINTER_DOWN) {
+                    // We do not support multi touch for back gesture
+                    cancelGesture(ev);
+                    return;
+                } else if (action == MotionEvent.ACTION_MOVE) {
+                    if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
+                        cancelGesture(ev);
+                        return;
+                    }
+                    float dx = Math.abs(ev.getX() - mDownPoint.x);
+                    float dy = Math.abs(ev.getY() - mDownPoint.y);
+                    if (dy > dx && dy > mTouchSlop) {
+                        cancelGesture(ev);
+                        return;
+                    } else if (dx > dy && dx > mTouchSlop) {
+                        mThresholdCrossed = true;
+                    }
+                }
+
+            }
+
+            // forward touch
+            mEdgeBackPanel.onMotionEvent(ev);
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            float dx = Math.abs(ev.getX() - mDownPoint.x);
+            float dy = Math.abs(ev.getY() - mDownPoint.y);
+            if (dx > dy && dx > mTouchSlop && !mAllowGesture && mGestureCallback != null) {
+                mGestureCallback.onBackGestureAttempted(mDisallowedGestureReason);
+            }
+        }
+    }
+
+    void setInsets(int leftInset, int rightInset) {
+        mLeftInset = leftInset;
+        mRightInset = rightInset;
+    }
+
+    enum BackGestureResult {
+        UNKNOWN,
+        BACK_COMPLETED_FROM_LEFT,
+        BACK_COMPLETED_FROM_RIGHT,
+        BACK_CANCELLED_FROM_LEFT,
+        BACK_CANCELLED_FROM_RIGHT,
+        BACK_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        BACK_NOT_STARTED_IN_NAV_BAR_REGION,
+    }
+
+    /** Callback to let the UI react to attempted back gestures. */
+    interface BackGestureAttemptCallback {
+        /** Called whenever any touch is completed. */
+        void onBackGestureAttempted(BackGestureResult result);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
new file mode 100644
index 0000000..0521db4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.interaction;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.core.math.MathUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
+
+/** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */
+public class EdgeBackGesturePanel extends View {
+
+    private static final String LOG_TAG = "EdgeBackGesturePanel";
+
+    private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80;
+    private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
+
+    /**
+     * The time required since the first vibration effect to automatically trigger a click
+     */
+    private static final int GESTURE_DURATION_FOR_CLICK_MS = 400;
+
+    /**
+     * The basic translation in dp where the arrow resides
+     */
+    private static final int BASE_TRANSLATION_DP = 32;
+
+    /**
+     * The length of the arrow leg measured from the center to the end
+     */
+    private static final int ARROW_LENGTH_DP = 18;
+
+    /**
+     * The angle measured from the xAxis, where the leg is when the arrow rests
+     */
+    private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
+
+    /**
+     * The angle that is added per 1000 px speed to the angle of the leg
+     */
+    private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 4;
+
+    /**
+     * The maximum angle offset allowed due to speed
+     */
+    private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
+
+    /**
+     * The thickness of the arrow. Adjusted to match the home handle (approximately)
+     */
+    private static final float ARROW_THICKNESS_DP = 2.5f;
+
+    /**
+     * The amount of rubber banding we do for the vertical translation
+     */
+    private static final int RUBBER_BAND_AMOUNT = 15;
+
+    /**
+     * The interpolator used to rubberband
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR =
+            new PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f);
+
+    /**
+     * The amount of rubber banding we do for the translation before base translation
+     */
+    private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
+
+    /**
+     * The interpolator used to rubberband the appearing of the arrow.
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR =
+            new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
+
+    private BackCallback mBackCallback;
+
+    /**
+     * The paint the arrow is drawn with
+     */
+    private final Paint mPaint = new Paint();
+
+    private final float mDensity;
+    private final float mBaseTranslation;
+    private final float mArrowLength;
+    private final float mArrowThickness;
+
+    /**
+     * The minimum delta needed in movement for the arrow to change direction / stop triggering back
+     */
+    private final float mMinDeltaForSwitch;
+    // The closest to y = 0 that the arrow will be displayed.
+    private int mMinArrowPosition;
+    // The amount the arrow is shifted to avoid the finger.
+    private int mFingerOffset;
+
+    private final float mSwipeThreshold;
+    private final Path mArrowPath = new Path();
+    private final Point mDisplaySize = new Point();
+
+    private final SpringAnimation mAngleAnimation;
+    private final SpringAnimation mTranslationAnimation;
+    private final SpringAnimation mVerticalTranslationAnimation;
+    private final SpringForce mAngleAppearForce;
+    private final SpringForce mAngleDisappearForce;
+    private final ValueAnimator mArrowDisappearAnimation;
+    private final SpringForce mRegularTranslationSpring;
+    private final SpringForce mTriggerBackSpring;
+
+    private VelocityTracker mVelocityTracker;
+    private int mArrowPaddingEnd;
+
+    /**
+     * True if the panel is currently on the left of the screen
+     */
+    private boolean mIsLeftPanel;
+
+    private float mStartX;
+    private float mStartY;
+    private float mCurrentAngle;
+    /**
+     * The current translation of the arrow
+     */
+    private float mCurrentTranslation;
+    /**
+     * Where the arrow will be in the resting position.
+     */
+    private float mDesiredTranslation;
+
+    private boolean mDragSlopPassed;
+    private boolean mArrowsPointLeft;
+    private float mMaxTranslation;
+    private boolean mTriggerBack;
+    private float mPreviousTouchTranslation;
+    private float mTotalTouchDelta;
+    private float mVerticalTranslation;
+    private float mDesiredVerticalTranslation;
+    private float mDesiredAngle;
+    private float mAngleOffset;
+    private float mDisappearAmount;
+    private long mVibrationTime;
+    private int mScreenSize;
+
+    private final DynamicAnimation.OnAnimationEndListener mSetGoneEndListener =
+            new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(
+                        DynamicAnimation animation, boolean canceled, float value, float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        setVisibility(GONE);
+                    }
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_ANGLE =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentAngle") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentAngle(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentAngle();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentTranslation") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentTranslation();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_VERTICAL_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("verticalTranslation") {
+
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setVerticalTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getVerticalTranslation();
+                }
+            };
+
+    public EdgeBackGesturePanel(Context context, ViewGroup parent, LayoutParams layoutParams) {
+        super(context);
+
+        mDensity = context.getResources().getDisplayMetrics().density;
+
+        mBaseTranslation = dp(BASE_TRANSLATION_DP);
+        mArrowLength = dp(ARROW_LENGTH_DP);
+        mArrowThickness = dp(ARROW_THICKNESS_DP);
+        mMinDeltaForSwitch = dp(32);
+
+        mPaint.setStrokeWidth(mArrowThickness);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeJoin(Paint.Join.ROUND);
+
+        mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
+        mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mArrowDisappearAnimation.addUpdateListener(animation -> {
+            mDisappearAmount = (float) animation.getAnimatedValue();
+            invalidate();
+        });
+
+        mAngleAnimation =
+                new SpringAnimation(this, CURRENT_ANGLE);
+        mAngleAppearForce = new SpringForce()
+                .setStiffness(500)
+                .setDampingRatio(0.5f);
+        mAngleDisappearForce = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setFinalPosition(90);
+        mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
+
+        mTranslationAnimation =
+                new SpringAnimation(this, CURRENT_TRANSLATION);
+        mRegularTranslationSpring = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTriggerBackSpring = new SpringForce()
+                .setStiffness(450)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        mVerticalTranslationAnimation =
+                new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
+        mVerticalTranslationAnimation.setSpring(
+                new SpringForce()
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+        int currentNightMode =
+                context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+        mPaint.setColor(context.getColor(currentNightMode == Configuration.UI_MODE_NIGHT_YES
+                ? R.color.back_arrow_color_light
+                : R.color.back_arrow_color_dark));
+        loadDimens();
+        updateArrowDirection();
+
+        mSwipeThreshold = ResourceUtils.getDimenByName(
+            "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */);
+        parent.addView(this, layoutParams);
+        setVisibility(GONE);
+    }
+
+    void onDestroy() {
+        ViewGroup parent = (ViewGroup) getParent();
+        if (parent != null) {
+            parent.removeView(this);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @SuppressLint("RtlHardcoded")
+    void setIsLeftPanel(boolean isLeftPanel) {
+        mIsLeftPanel = isLeftPanel;
+    }
+
+    boolean getIsLeftPanel() {
+        return mIsLeftPanel;
+    }
+
+    void setDisplaySize(Point displaySize) {
+        mDisplaySize.set(displaySize.x, displaySize.y);
+        mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
+    }
+
+    void setBackCallback(BackCallback callback) {
+        mBackCallback = callback;
+    }
+
+    private float getCurrentAngle() {
+        return mCurrentAngle;
+    }
+
+    private float getCurrentTranslation() {
+        return mCurrentTranslation;
+    }
+
+    void onMotionEvent(MotionEvent event) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mDragSlopPassed = false;
+                resetOnDown();
+                mStartX = event.getX();
+                mStartY = event.getY();
+                setVisibility(VISIBLE);
+                updatePosition(event.getY());
+                break;
+            case MotionEvent.ACTION_MOVE:
+                handleMoveEvent(event);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mTriggerBack) {
+                    triggerBack();
+                } else {
+                    cancelBack();
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancelBack();
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateArrowDirection();
+        loadDimens();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
+        canvas.save();
+        canvas.translate(
+                mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
+                (getHeight() * 0.5f) + mVerticalTranslation);
+
+        // Let's calculate the position of the end based on the angle
+        float x = (polarToCartX(mCurrentAngle) * mArrowLength);
+        float y = (polarToCartY(mCurrentAngle) * mArrowLength);
+        Path arrowPath = calculatePath(x, y);
+
+        canvas.drawPath(arrowPath, mPaint);
+        canvas.restore();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mMaxTranslation = getWidth() - mArrowPaddingEnd;
+    }
+
+    private void loadDimens() {
+        Resources res = getResources();
+        mArrowPaddingEnd = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8);
+        mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64);
+        mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48);
+    }
+
+    private void updateArrowDirection() {
+        // Both panels arrow point the same way
+        mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+        invalidate();
+    }
+
+    private float getStaticArrowWidth() {
+        return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
+    }
+
+    private float polarToCartX(float angleInDegrees) {
+        return (float) Math.cos(Math.toRadians(angleInDegrees));
+    }
+
+    private float polarToCartY(float angleInDegrees) {
+        return (float) Math.sin(Math.toRadians(angleInDegrees));
+    }
+
+    private Path calculatePath(float x, float y) {
+        if (!mArrowsPointLeft) {
+            x = -x;
+        }
+        float extent = lerp(1.0f, 0.75f, mDisappearAmount);
+        x = x * extent;
+        y = y * extent;
+        mArrowPath.reset();
+        mArrowPath.moveTo(x, y);
+        mArrowPath.lineTo(0, 0);
+        mArrowPath.lineTo(x, -y);
+        return mArrowPath;
+    }
+
+    private static float lerp(float start, float stop, float amount) {
+        return start + (stop - start) * amount;
+    }
+
+    private void triggerBack() {
+        if (mBackCallback != null) {
+            mBackCallback.triggerBack();
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        // Only do the extra translation if we're not already flinging
+        boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500;
+        if (isSlow
+                || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+        }
+
+        // Let's also snap the angle a bit
+        if (mAngleOffset > -4) {
+            mAngleOffset = Math.max(-8, mAngleOffset - 8);
+            updateAngle(true /* animated */);
+        }
+
+        // Finally, after the translation, animate back and disappear the arrow
+        Runnable translationEnd = () -> {
+            // let's snap it back
+            mAngleOffset = Math.max(0, mAngleOffset + 8);
+            updateAngle(true /* animated */);
+
+            mTranslationAnimation.setSpring(mTriggerBackSpring);
+            // Translate the arrow back a bit to make for a nice transition
+            setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */);
+            animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
+                    .withEndAction(() -> setVisibility(GONE));
+            mArrowDisappearAnimation.start();
+        };
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
+                        float value,
+                        float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        translationEnd.run();
+                    }
+                }
+            });
+        } else {
+            translationEnd.run();
+        }
+    }
+
+    private void cancelBack() {
+        if (mBackCallback != null) {
+            mBackCallback.cancelBack();
+        }
+
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(mSetGoneEndListener);
+        } else {
+            setVisibility(GONE);
+        }
+    }
+
+    private void resetOnDown() {
+        animate().cancel();
+        mAngleAnimation.cancel();
+        mTranslationAnimation.cancel();
+        mVerticalTranslationAnimation.cancel();
+        mArrowDisappearAnimation.cancel();
+        mAngleOffset = 0;
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        // Reset the arrow to the side
+        setTriggerBack(false /* triggerBack */, false /* animated */);
+        setDesiredTranslation(0, false /* animated */);
+        setCurrentTranslation(0);
+        updateAngle(false /* animate */);
+        mPreviousTouchTranslation = 0;
+        mTotalTouchDelta = 0;
+        mVibrationTime = 0;
+        setDesiredVerticalTransition(0, false /* animated */);
+    }
+
+    private void handleMoveEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        float touchTranslation = Math.abs(x - mStartX);
+        float yOffset = y - mStartY;
+        float delta = touchTranslation - mPreviousTouchTranslation;
+        if (Math.abs(delta) > 0) {
+            if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
+                mTotalTouchDelta += delta;
+            } else {
+                mTotalTouchDelta = delta;
+            }
+        }
+        mPreviousTouchTranslation = touchTranslation;
+
+        // Apply a haptic on drag slop passed
+        if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
+            mDragSlopPassed = true;
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+            mVibrationTime = SystemClock.uptimeMillis();
+
+            // Let's show the arrow and animate it in!
+            mDisappearAmount = 0.0f;
+            setAlpha(1f);
+            // And animate it go to back by default!
+            setTriggerBack(true /* triggerBack */, true /* animated */);
+        }
+
+        // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
+        if (touchTranslation > mBaseTranslation) {
+            float diff = touchTranslation - mBaseTranslation;
+            float progress = MathUtils.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                    * (mMaxTranslation - mBaseTranslation);
+            touchTranslation = mBaseTranslation + progress;
+        } else {
+            float diff = mBaseTranslation - touchTranslation;
+            float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
+                    * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
+            touchTranslation = mBaseTranslation - progress;
+        }
+        // By default we just assume the current direction is kept
+        boolean triggerBack = mTriggerBack;
+
+        //  First lets see if we had continuous motion in one direction for a while
+        if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
+            triggerBack = mTotalTouchDelta > 0;
+        }
+
+        // Then, let's see if our velocity tells us to change direction
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float xVelocity = mVelocityTracker.getXVelocity();
+        float yVelocity = mVelocityTracker.getYVelocity();
+        float velocity = (float) Math.hypot(xVelocity, yVelocity);
+        mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
+                ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
+        if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
+            mAngleOffset *= -1;
+        }
+
+        // Last if the direction in Y is bigger than X * 2 we also abort
+        if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+            triggerBack = false;
+        }
+        setTriggerBack(triggerBack, true /* animated */);
+
+        if (!mTriggerBack) {
+            touchTranslation = 0;
+        } else if (mIsLeftPanel && mArrowsPointLeft
+                || (!mIsLeftPanel && !mArrowsPointLeft)) {
+            // If we're on the left we should move less, because the arrow is facing the other
+            // direction
+            touchTranslation -= getStaticArrowWidth();
+        }
+        setDesiredTranslation(touchTranslation, true /* animated */);
+        updateAngle(true /* animated */);
+
+        float maxYOffset = getHeight() / 2.0f - mArrowLength;
+        float progress =
+                MathUtils.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1);
+        float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                * maxYOffset * Math.signum(yOffset);
+        setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+    }
+
+    private void updatePosition(float touchY) {
+        float positionY = touchY - mFingerOffset;
+        positionY = Math.max(positionY, mMinArrowPosition);
+        positionY -= getLayoutParams().height / 2.0f;
+        setX(mIsLeftPanel ? 0 : mDisplaySize.x - getLayoutParams().width);
+        setY(MathUtils.clamp((int) positionY, 0, mDisplaySize.y));
+    }
+
+    private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
+        if (mDesiredVerticalTranslation != verticalTranslation) {
+            mDesiredVerticalTranslation = verticalTranslation;
+            if (!animated) {
+                setVerticalTranslation(verticalTranslation);
+            } else {
+                mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
+            }
+            invalidate();
+        }
+    }
+
+    private void setVerticalTranslation(float verticalTranslation) {
+        mVerticalTranslation = verticalTranslation;
+        invalidate();
+    }
+
+    private float getVerticalTranslation() {
+        return mVerticalTranslation;
+    }
+
+    private void setDesiredTranslation(float desiredTranslation, boolean animated) {
+        if (mDesiredTranslation != desiredTranslation) {
+            mDesiredTranslation = desiredTranslation;
+            if (!animated) {
+                setCurrentTranslation(desiredTranslation);
+            } else {
+                mTranslationAnimation.animateToFinalPosition(desiredTranslation);
+            }
+        }
+    }
+
+    private void setCurrentTranslation(float currentTranslation) {
+        mCurrentTranslation = currentTranslation;
+        invalidate();
+    }
+
+    private void setTriggerBack(boolean triggerBack, boolean animated) {
+        if (mTriggerBack != triggerBack) {
+            mTriggerBack = triggerBack;
+            mAngleAnimation.cancel();
+            updateAngle(animated);
+            // Whenever the trigger back state changes the existing translation animation should be
+            // cancelled
+            mTranslationAnimation.cancel();
+        }
+    }
+
+    private void updateAngle(boolean animated) {
+        float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
+        if (newAngle != mDesiredAngle) {
+            if (!animated) {
+                setCurrentAngle(newAngle);
+            } else {
+                mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
+                mAngleAnimation.animateToFinalPosition(newAngle);
+            }
+            mDesiredAngle = newAngle;
+        }
+    }
+
+    private void setCurrentAngle(float currentAngle) {
+        mCurrentAngle = currentAngle;
+        invalidate();
+    }
+
+    private float dp(float dp) {
+        return mDensity * dp;
+    }
+
+    /** Callback to let the gesture handler react to the detected back gestures. */
+    interface BackCallback {
+        /** Indicates that a Back gesture was recognized. */
+        void triggerBack();
+
+        /** Indicates that the gesture was cancelled. */
+        void cancelBack();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
new file mode 100644
index 0000000..f8d9d8d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialFragment.KEY_TUTORIAL_TYPE;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.View;
+import android.view.Window;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+import java.util.List;
+
+/** Shows the gesture interactive sandbox in full screen mode. */
+public class GestureSandboxActivity extends FragmentActivity {
+
+    private static final String LOG_TAG = "GestureSandboxActivity";
+
+    private TutorialFragment mFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.gesture_tutorial_activity);
+
+        mFragment = TutorialFragment.newInstance(getTutorialType(getIntent().getExtras()));
+        getSupportFragmentManager().beginTransaction()
+                .add(R.id.gesture_tutorial_fragment_container, mFragment)
+                .commit();
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        disableSystemGestures();
+        mFragment.onAttachedToWindow();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mFragment.onDetachedFromWindow();
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            hideSystemUI();
+        }
+    }
+
+    private TutorialType getTutorialType(Bundle extras) {
+        TutorialType defaultType = TutorialType.RIGHT_EDGE_BACK_NAVIGATION;
+
+        if (extras == null || !extras.containsKey(KEY_TUTORIAL_TYPE)) {
+            return defaultType;
+        }
+        try {
+            return TutorialType.valueOf(extras.getString(KEY_TUTORIAL_TYPE, ""));
+        } catch (IllegalArgumentException e) {
+            return defaultType;
+        }
+    }
+
+    private void hideSystemUI() {
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                        | View.SYSTEM_UI_FLAG_FULLSCREEN);
+        getWindow().setNavigationBarColor(Color.TRANSPARENT);
+    }
+
+    private void disableSystemGestures() {
+        Display display = getDisplay();
+        if (display != null) {
+            DisplayMetrics metrics = new DisplayMetrics();
+            display.getMetrics(metrics);
+            getWindow().setSystemGestureExclusionRects(
+                    List.of(new Rect(0, 0, metrics.widthPixels, metrics.heightPixels)));
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
new file mode 100644
index 0000000..0edabd4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
+
+import android.annotation.TargetApi;
+import android.graphics.PointF;
+import android.os.Build;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Home tutorial. */
+@TargetApi(Build.VERSION_CODES.R)
+final class HomeGestureTutorialController extends SwipeUpGestureTutorialController {
+
+    HomeGestureTutorialController(HomeGestureTutorialFragment fragment, TutorialType tutorialType) {
+        super(fragment, tutorialType);
+    }
+
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case HOME_NAVIGATION:
+                return R.string.home_gesture_tutorial_playground_title;
+            case HOME_NAVIGATION_COMPLETE:
+                return R.string.gesture_tutorial_confirm_title;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getSubtitleStringId() {
+        if (mTutorialType == TutorialType.HOME_NAVIGATION) {
+            return R.string.home_gesture_tutorial_playground_subtitle;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == HOME_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_done;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
+            case HOME_NAVIGATION:
+                switch (result) {
+                    case BACK_COMPLETED_FROM_LEFT:
+                    case BACK_COMPLETED_FROM_RIGHT:
+                    case BACK_CANCELLED_FROM_LEFT:
+                    case BACK_CANCELLED_FROM_RIGHT:
+                        showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
+                        break;
+                }
+                break;
+            case HOME_NAVIGATION_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+        switch (mTutorialType) {
+            case HOME_NAVIGATION:
+                switch (result) {
+                    case HOME_GESTURE_COMPLETED: {
+                        animateFakeTaskViewHome(finalVelocity, () ->
+                                mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE));
+                        break;
+                    }
+                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                        showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
+                        break;
+                    case OVERVIEW_GESTURE_COMPLETED:
+                        fadeOutFakeTaskView(true, () ->
+                                showFeedback(R.string.home_gesture_feedback_overview_detected));
+                        break;
+                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+                    case HOME_OR_OVERVIEW_CANCELLED:
+                        fadeOutFakeTaskView(false, null);
+                        showFeedback(R.string.home_gesture_feedback_wrong_swipe_direction);
+                        break;
+                }
+                break;
+            case HOME_NAVIGATION_COMPLETE:
+                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
new file mode 100644
index 0000000..e2a9d12
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the Home gesture interactive tutorial. */
+public class HomeGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.home_gesture;
+    }
+
+    @Override
+    TutorialController createController(TutorialType type) {
+        return new HomeGestureTutorialController(this, type);
+    }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return HomeGestureTutorialController.class;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
new file mode 100644
index 0000000..0e2312b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+import com.android.systemui.shared.system.QuickStepContract;
+
+/** Utility class to handle Home and Assistant gestures. */
+public class NavBarGestureHandler implements OnTouchListener,
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+
+    private static final String LOG_TAG = "NavBarGestureHandler";
+    private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
+
+    private final Context mContext;
+    private final Point mDisplaySize = new Point();
+    private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
+    private final int mBottomGestureHeight;
+    private final GestureDetector mAssistantGestureDetector;
+    private final int mAssistantAngleThreshold;
+    private final RectF mAssistantLeftRegion = new RectF();
+    private final RectF mAssistantRightRegion = new RectF();
+    private final float mAssistantDragDistThreshold;
+    private final float mAssistantFlingDistThreshold;
+    private final long mAssistantTimeThreshold;
+    private final float mAssistantSquaredSlop;
+    private final PointF mAssistantStartDragPos = new PointF();
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private boolean mTouchCameFromAssistantCorner;
+    private boolean mTouchCameFromNavBar;
+    private boolean mPassedAssistantSlop;
+    private boolean mAssistantGestureActive;
+    private boolean mLaunchedAssistant;
+    private long mAssistantDragStartTime;
+    private float mAssistantDistance;
+    private float mAssistantTimeFraction;
+    private float mAssistantLastProgress;
+    @Nullable
+    private NavBarGestureAttemptCallback mGestureCallback;
+
+    NavBarGestureHandler(Context context) {
+        mContext = context;
+        final Display display = mContext.getDisplay();
+        final int displayRotation;
+        if (display == null) {
+            displayRotation = Surface.ROTATION_0;
+        } else {
+            displayRotation = display.getRotation();
+            display.getRealSize(mDisplaySize);
+        }
+        mSwipeUpTouchTracker =
+                new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
+                        new NavBarPosition(Mode.NO_BUTTON, displayRotation),
+                        null /*onInterceptTouch*/, this);
+
+        final Resources resources = context.getResources();
+        mBottomGestureHeight =
+                ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
+        mAssistantDragDistThreshold =
+                resources.getDimension(R.dimen.gestures_assistant_drag_threshold);
+        mAssistantFlingDistThreshold =
+                resources.getDimension(R.dimen.gestures_assistant_fling_threshold);
+        mAssistantTimeThreshold =
+                resources.getInteger(R.integer.assistant_gesture_min_time_threshold);
+        mAssistantAngleThreshold =
+                resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+
+        mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener());
+        int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
+        final float assistantHeight = Math.max(mBottomGestureHeight,
+                QuickStepContract.getWindowCornerRadius(resources));
+        mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y;
+        mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight;
+        mAssistantLeftRegion.left = 0;
+        mAssistantLeftRegion.right = assistantWidth;
+        mAssistantRightRegion.right = mDisplaySize.x;
+        mAssistantRightRegion.left = mDisplaySize.x - assistantWidth;
+        float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mAssistantSquaredSlop = slop * slop;
+    }
+
+    void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
+        mGestureCallback = callback;
+    }
+
+    void unregisterNavBarGestureAttemptCallback() {
+        mGestureCallback = null;
+    }
+
+    @Override
+    public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+        if (mGestureCallback == null || mAssistantGestureActive) {
+            return;
+        }
+        finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000);
+        if (mTouchCameFromNavBar) {
+            mGestureCallback.onNavBarGestureAttempted(wasFling
+                    ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity);
+        } else {
+            mGestureCallback.onNavBarGestureAttempted(wasFling
+                    ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
+                    finalVelocity);
+        }
+    }
+
+    @Override
+    public void onSwipeUpCancelled() {
+        if (mGestureCallback != null && !mAssistantGestureActive) {
+            mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
+        }
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        int action = event.getAction();
+        boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mDownPos.set(event.getX(), event.getY());
+                mLastPos.set(mDownPos);
+                mTouchCameFromAssistantCorner =
+                        mAssistantLeftRegion.contains(event.getX(), event.getY())
+                                || mAssistantRightRegion.contains(event.getX(), event.getY());
+                mAssistantGestureActive = mTouchCameFromAssistantCorner;
+                mTouchCameFromNavBar = !mTouchCameFromAssistantCorner
+                        && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
+                if (!mTouchCameFromNavBar && mGestureCallback != null) {
+                    mGestureCallback.setNavBarGestureProgress(null);
+                }
+                mLaunchedAssistant = false;
+                mSwipeUpTouchTracker.init();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!mAssistantGestureActive) {
+                    break;
+                }
+                mLastPos.set(event.getX(), event.getY());
+
+                if (!mPassedAssistantSlop) {
+                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
+                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+                            > mAssistantSquaredSlop) {
+
+                        mPassedAssistantSlop = true;
+                        mAssistantStartDragPos.set(mLastPos.x, mLastPos.y);
+                        mAssistantDragStartTime = SystemClock.uptimeMillis();
+
+                        mAssistantGestureActive = isValidAssistantGestureAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y);
+                        if (!mAssistantGestureActive && mGestureCallback != null) {
+                            mGestureCallback.onNavBarGestureAttempted(
+                                    ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF());
+                        }
+                    }
+                } else {
+                    // Movement
+                    mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x,
+                            mLastPos.y - mAssistantStartDragPos.y);
+                    if (mAssistantDistance >= 0) {
+                        final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime;
+                        mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1);
+                        updateAssistantProgress();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
+                    mGestureCallback.onNavBarGestureAttempted(
+                            HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
+                    intercepted = true;
+                    break;
+                }
+                if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) {
+                    mGestureCallback.onNavBarGestureAttempted(
+                            ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF());
+                    ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0)
+                            .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS);
+                    animator.addUpdateListener(valueAnimator -> {
+                        float progress = (float) valueAnimator.getAnimatedValue();
+                        mGestureCallback.setAssistantProgress(progress);
+                    });
+                    animator.setInterpolator(Interpolators.DEACCEL_2);
+                    animator.start();
+                }
+                mPassedAssistantSlop = false;
+                break;
+        }
+        if (mTouchCameFromNavBar && mGestureCallback != null) {
+            mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y);
+        }
+        mSwipeUpTouchTracker.onMotionEvent(event);
+        mAssistantGestureDetector.onTouchEvent(event);
+        return intercepted;
+    }
+
+    /**
+     * Determine if angle is larger than threshold for assistant detection
+     */
+    private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
+        float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+
+        // normalize so that angle is measured clockwise from horizontal in the bottom right corner
+        // and counterclockwise from horizontal in the bottom left corner
+        angle = angle > 90 ? 180 - angle : angle;
+        return (angle > mAssistantAngleThreshold && angle < 90);
+    }
+
+    private void updateAssistantProgress() {
+        if (!mLaunchedAssistant) {
+            mAssistantLastProgress =
+                    Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1)
+                            * mAssistantTimeFraction;
+            if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) {
+                startAssistant(new PointF());
+            } else if (mGestureCallback != null) {
+                mGestureCallback.setAssistantProgress(mAssistantLastProgress);
+            }
+        }
+    }
+
+    private void startAssistant(PointF velocity) {
+        if (mGestureCallback != null) {
+            mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity);
+        }
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK);
+        mLaunchedAssistant = true;
+    }
+
+    enum NavBarGestureResult {
+        UNKNOWN,
+        HOME_GESTURE_COMPLETED,
+        OVERVIEW_GESTURE_COMPLETED,
+        HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION,  // Side swipe on nav bar.
+        HOME_OR_OVERVIEW_CANCELLED,
+        ASSISTANT_COMPLETED,
+        ASSISTANT_NOT_STARTED_BAD_ANGLE,
+        ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT,
+    }
+
+    /** Callback to let the UI react to attempted nav bar gestures. */
+    interface NavBarGestureAttemptCallback {
+        /** Called whenever any touch is completed. */
+        void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
+
+        /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
+        default void setNavBarGestureProgress(@Nullable Float displacement) {}
+
+        /** Indicates the progress of an Assistant gesture. */
+        default void setAssistantProgress(float progress) {}
+    }
+
+    private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) {
+                PointF velocity = new PointF(velocityX, velocityY);
+                if (!isValidAssistantGestureAngle(velocityX, -velocityY)) {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE,
+                                velocity);
+                    }
+                } else if (mAssistantDistance >= mAssistantFlingDistThreshold) {
+                    mAssistantLastProgress = 1;
+                    startAssistant(velocity);
+                }
+            }
+            return true;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
new file mode 100644
index 0000000..c636eba
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
+
+import android.annotation.TargetApi;
+import android.graphics.PointF;
+import android.os.Build;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Overview tutorial. */
+@TargetApi(Build.VERSION_CODES.R)
+final class OverviewGestureTutorialController extends SwipeUpGestureTutorialController {
+
+    OverviewGestureTutorialController(OverviewGestureTutorialFragment fragment,
+            TutorialType tutorialType) {
+        super(fragment, tutorialType);
+    }
+
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case OVERVIEW_NAVIGATION:
+                return R.string.overview_gesture_tutorial_playground_title;
+            case OVERVIEW_NAVIGATION_COMPLETE:
+                return R.string.gesture_tutorial_confirm_title;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getSubtitleStringId() {
+        if (mTutorialType == TutorialType.OVERVIEW_NAVIGATION) {
+            return R.string.overview_gesture_tutorial_playground_subtitle;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_done;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
+            case OVERVIEW_NAVIGATION:
+                switch (result) {
+                    case BACK_COMPLETED_FROM_LEFT:
+                    case BACK_COMPLETED_FROM_RIGHT:
+                    case BACK_CANCELLED_FROM_LEFT:
+                    case BACK_CANCELLED_FROM_RIGHT:
+                        showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
+                        break;
+                }
+                break;
+            case OVERVIEW_NAVIGATION_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+        switch (mTutorialType) {
+            case OVERVIEW_NAVIGATION:
+                switch (result) {
+                    case HOME_GESTURE_COMPLETED: {
+                        animateFakeTaskViewHome(finalVelocity, () ->
+                                showFeedback(R.string.overview_gesture_feedback_home_detected));
+                        break;
+                    }
+                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                        showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
+                        break;
+                    case OVERVIEW_GESTURE_COMPLETED:
+                        fadeOutFakeTaskView(true, () ->
+                                mTutorialFragment.changeController(OVERVIEW_NAVIGATION_COMPLETE));
+                        break;
+                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+                    case HOME_OR_OVERVIEW_CANCELLED:
+                        fadeOutFakeTaskView(false, null);
+                        showFeedback(R.string.overview_gesture_feedback_wrong_swipe_direction);
+                        break;
+                }
+                break;
+            case OVERVIEW_NAVIGATION_COMPLETE:
+                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
new file mode 100644
index 0000000..3357b70
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the Overview gesture interactive tutorial. */
+public class OverviewGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.overview_gesture;
+    }
+
+    @Override
+    TutorialController createController(TutorialType type) {
+        return new OverviewGestureTutorialController(this, type);
+    }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return OverviewGestureTutorialController.class;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
new file mode 100644
index 0000000..14e00dc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
+import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
+import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SwipeUpAnimationLogic;
+import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.TransformParams;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+@TargetApi(Build.VERSION_CODES.R)
+abstract class SwipeUpGestureTutorialController extends TutorialController {
+    private final ViewSwipeUpAnimation mViewSwipeUpAnimation;
+    private float mFakeTaskViewRadius;
+    private Rect mFakeTaskViewRect = new Rect();
+    private RunningWindowAnim mRunningWindowAnim;
+
+    SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
+        super(tutorialFragment, tutorialType);
+        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
+        OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState);
+        mViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
+                new GestureState(observer, -1));
+        observer.onDestroy();
+        deviceState.destroy();
+
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext)
+                .getDeviceProfile(mContext)
+                .copy(mContext);
+        Insets insets = mContext.getSystemService(WindowManager.class)
+                .getCurrentWindowMetrics()
+                .getWindowInsets()
+                .getInsets(WindowInsets.Type.systemBars());
+        dp.updateInsets(new Rect(insets.left, insets.top, insets.right, insets.bottom));
+        mViewSwipeUpAnimation.initDp(dp);
+
+        mFakeTaskViewRadius = QuickStepContract.getWindowCornerRadius(mContext.getResources());
+        mFakeTaskView.setClipToOutline(true);
+        mFakeTaskView.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius);
+            }
+        });
+    }
+
+    private void cancelRunningAnimation() {
+        if (mRunningWindowAnim != null) {
+            mRunningWindowAnim.cancel();
+        }
+        mRunningWindowAnim = null;
+    }
+
+    /** Fades the task view, optionally after animating to a fake Overview. */
+    void fadeOutFakeTaskView(boolean toOverviewFirst, @Nullable Runnable onEndRunnable) {
+        hideFeedback();
+        hideHandCoachingAnimation();
+        cancelRunningAnimation();
+        PendingAnimation anim = new PendingAnimation(300);
+        AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation, boolean isReverse) {
+                mFakeTaskView.setVisibility(View.INVISIBLE);
+                mFakeTaskView.setAlpha(1);
+                mRunningWindowAnim = null;
+            }
+        };
+        if (toOverviewFirst) {
+            anim.setFloat(mViewSwipeUpAnimation.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation, boolean isReverse) {
+                    PendingAnimation fadeAnim = new PendingAnimation(300);
+                    fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+                    fadeAnim.addListener(resetTaskView);
+                    AnimatorSet animset = fadeAnim.buildAnim();
+                    animset.setStartDelay(100);
+                    animset.start();
+                    mRunningWindowAnim = RunningWindowAnim.wrap(animset);
+                }
+            });
+        } else {
+            anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+            anim.addListener(resetTaskView);
+        }
+        if (onEndRunnable != null) {
+            anim.addListener(AnimationSuccessListener.forRunnable(onEndRunnable));
+        }
+        AnimatorSet animset = anim.buildAnim();
+        animset.start();
+        mRunningWindowAnim = RunningWindowAnim.wrap(animset);
+    }
+
+    void animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable) {
+        hideFeedback();
+        hideHandCoachingAnimation();
+        cancelRunningAnimation();
+        RectFSpringAnim rectAnim =
+                mViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
+        // After home animation finishes, fade out and run onEndRunnable.
+        rectAnim.addAnimatorListener(AnimationSuccessListener.forRunnable(
+                () -> fadeOutFakeTaskView(false, onEndRunnable)));
+        mRunningWindowAnim = RunningWindowAnim.wrap(rectAnim);
+    }
+
+    @Override
+    public void setNavBarGestureProgress(@Nullable Float displacement) {
+        if (displacement == null || mTutorialType == HOME_NAVIGATION_COMPLETE
+                || mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
+            mFakeTaskView.setVisibility(View.INVISIBLE);
+        } else {
+            mFakeTaskView.setVisibility(View.VISIBLE);
+            if (mRunningWindowAnim == null) {
+                mViewSwipeUpAnimation.updateDisplacement(displacement);
+            }
+        }
+    }
+
+    class ViewSwipeUpAnimation extends SwipeUpAnimationLogic {
+
+        ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState,
+                             GestureState gestureState) {
+            super(context, deviceState, gestureState, new FakeTransformParams());
+        }
+
+        void initDp(DeviceProfile dp) {
+            initTransitionEndpoints(dp);
+            mTaskViewSimulator.setPreviewBounds(
+                    new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets());
+        }
+
+        @Override
+        public void updateFinalShift() {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
+            mTaskViewSimulator.apply(mTransformParams);
+        }
+
+        AnimatedFloat getCurrentShift() {
+            return mCurrentShift;
+        }
+
+        RectFSpringAnim handleSwipeUpToHome(PointF velocity) {
+            PointF velocityPxPerMs = new PointF(velocity.x, velocity.y);
+            float currentShift = mCurrentShift.value;
+            final float startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
+                    * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
+            float distanceToTravel = (1 - currentShift) * mTransitionDragLength;
+
+            // we want the page's snap velocity to approximately match the velocity at
+            // which the user flings, so we scale the duration by a value near to the
+            // derivative of the scroll interpolator at zero, ie. 2.
+            long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
+            long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+            HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory(null) {
+                @Override
+                public AnimatorPlaybackController createActivityAnimationToHome() {
+                    return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
+                }
+
+                @NonNull
+                @Override
+                public RectF getWindowTargetRect() {
+                    int fakeHomeIconSizePx = mDp.allAppsIconSizePx;
+                    int fakeHomeIconLeft = (mDp.widthPx - fakeHomeIconSizePx) / 2;
+                    int fakeHomeIconTop = mDp.heightPx - (mDp.allAppsCellHeightPx * 3);
+                    return new RectF(fakeHomeIconLeft, fakeHomeIconTop,
+                            fakeHomeIconLeft + fakeHomeIconSizePx,
+                            fakeHomeIconTop + fakeHomeIconSizePx);
+                }
+            };
+            RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
+            windowAnim.start(mContext, velocityPxPerMs);
+            return windowAnim;
+        }
+    }
+
+    private class FakeTransformParams extends TransformParams {
+
+        @Override
+        public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
+            SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null);
+            proxy.onBuildTargetParams(builder, null, this);
+            return new SurfaceParams[] {builder.build()};
+        }
+
+        @Override
+        public void applySurfaceParams(SurfaceParams[] params) {
+            SurfaceParams p = params[0];
+            mFakeTaskView.setAnimationMatrix(p.matrix);
+            mFakeTaskViewRect.set(p.windowCrop);
+            mFakeTaskViewRadius = p.cornerRadius;
+            mFakeTaskView.invalidateOutline();
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
new file mode 100644
index 0000000..c1918c2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.RippleDrawable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
+
+abstract class TutorialController implements BackGestureAttemptCallback,
+        NavBarGestureAttemptCallback {
+
+    private static final int FEEDBACK_VISIBLE_MS = 3000;
+    private static final int FEEDBACK_ANIMATION_MS = 500;
+    private static final int RIPPLE_VISIBLE_MS = 300;
+
+    final TutorialFragment mTutorialFragment;
+    TutorialType mTutorialType;
+    final Context mContext;
+
+    final ImageButton mCloseButton;
+    final TextView mTitleTextView;
+    final TextView mSubtitleTextView;
+    final TextView mFeedbackView;
+    final View mFakeTaskView;
+    final View mRippleView;
+    final RippleDrawable mRippleDrawable;
+    final TutorialHandAnimation mHandCoachingAnimation;
+    final ImageView mHandCoachingView;
+    final Button mActionTextButton;
+    final Button mActionButton;
+    private final Runnable mHideFeedbackRunnable;
+
+    TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
+        mTutorialFragment = tutorialFragment;
+        mTutorialType = tutorialType;
+        mContext = mTutorialFragment.getContext();
+
+        View rootView = tutorialFragment.getRootView();
+        mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
+        mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
+        mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
+        mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
+        mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+        mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
+        mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
+        mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
+        mHandCoachingAnimation = tutorialFragment.getHandAnimation();
+        mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
+        mHandCoachingView.bringToFront();
+        mActionTextButton =
+                rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
+        mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
+
+        mHideFeedbackRunnable =
+                () -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS)
+                        .withEndAction(this::showHandCoachingAnimation).start();
+    }
+
+    void setTutorialType(TutorialType tutorialType) {
+        mTutorialType = tutorialType;
+    }
+
+    @Nullable
+    Integer getTitleStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getSubtitleStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getActionButtonStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getActionTextButtonStringId() {
+        return null;
+    }
+
+    void showFeedback(int resId) {
+        hideHandCoachingAnimation();
+        mFeedbackView.setText(resId);
+        mFeedbackView.animate().alpha(1).setDuration(FEEDBACK_ANIMATION_MS).start();
+        mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
+        mFeedbackView.postDelayed(mHideFeedbackRunnable, FEEDBACK_VISIBLE_MS);
+    }
+
+    void hideFeedback() {
+        mFeedbackView.setText(null);
+        mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
+        mFeedbackView.clearAnimation();
+        mFeedbackView.setAlpha(0);
+    }
+
+    void setRippleHotspot(float x, float y) {
+        mRippleDrawable.setHotspot(x, y);
+    }
+
+    void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
+        mRippleDrawable.setState(
+                new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
+        mRippleView.postDelayed(() -> {
+            mRippleDrawable.setState(new int[] {});
+            if (onCompleteRunnable != null) {
+                onCompleteRunnable.run();
+            }
+        }, RIPPLE_VISIBLE_MS);
+    }
+
+    void onActionButtonClicked(View button) {}
+
+    void onActionTextButtonClicked(View button) {}
+
+    void showHandCoachingAnimation() {
+        if (isComplete()) {
+            return;
+        }
+        mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
+    }
+
+    void hideHandCoachingAnimation() {
+        mHandCoachingAnimation.stop();
+        mHandCoachingView.setVisibility(View.INVISIBLE);
+    }
+
+    @CallSuper
+    void transitToController() {
+        hideFeedback();
+        updateTitles();
+        updateActionButtons();
+
+        if (isComplete()) {
+            hideHandCoachingAnimation();
+        } else {
+            showHandCoachingAnimation();
+        }
+    }
+
+    private void updateTitles() {
+        updateTitleView(mTitleTextView, getTitleStringId(),
+                R.style.TextAppearance_GestureTutorial_Title);
+        updateTitleView(mSubtitleTextView, getSubtitleStringId(),
+                R.style.TextAppearance_GestureTutorial_Subtitle);
+    }
+
+    private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) {
+        if (stringId == null) {
+            textView.setVisibility(View.GONE);
+            return;
+        }
+
+        textView.setVisibility(View.VISIBLE);
+        textView.setText(stringId);
+        textView.setTextAppearance(styleId);
+    }
+
+    private void updateActionButtons() {
+        updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
+        updateButton(
+                mActionTextButton, getActionTextButtonStringId(), this::onActionTextButtonClicked);
+    }
+
+    private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) {
+        if (stringId == null) {
+            button.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        button.setVisibility(View.VISIBLE);
+        button.setText(stringId);
+        button.setOnClickListener(listener);
+    }
+
+    private boolean isComplete() {
+        return mTutorialType == TutorialType.BACK_NAVIGATION_COMPLETE
+                || mTutorialType == TutorialType.HOME_NAVIGATION_COMPLETE
+                || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE
+                || mTutorialType == TutorialType.ASSISTANT_COMPLETE;
+    }
+
+    /** Denotes the type of the tutorial. */
+    enum TutorialType {
+        RIGHT_EDGE_BACK_NAVIGATION,
+        LEFT_EDGE_BACK_NAVIGATION,
+        BACK_NAVIGATION_COMPLETE,
+        HOME_NAVIGATION,
+        HOME_NAVIGATION_COMPLETE,
+        OVERVIEW_NAVIGATION,
+        OVERVIEW_NAVIGATION_COMPLETE,
+        ASSISTANT,
+        ASSISTANT_COMPLETE
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
new file mode 100644
index 0000000..9a8264d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Intent;
+import android.graphics.Insets;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+abstract class TutorialFragment extends Fragment implements OnTouchListener {
+
+    private static final String LOG_TAG = "TutorialFragment";
+    static final String KEY_TUTORIAL_TYPE = "tutorial_type";
+
+    TutorialType mTutorialType;
+    @Nullable TutorialController mTutorialController = null;
+    View mRootView;
+    TutorialHandAnimation mHandCoachingAnimation;
+    EdgeBackGestureHandler mEdgeBackGestureHandler;
+    NavBarGestureHandler mNavBarGestureHandler;
+
+    public static TutorialFragment newInstance(TutorialType tutorialType) {
+        TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
+        if (fragment == null) {
+            fragment = new BackGestureTutorialFragment();
+            tutorialType = TutorialType.RIGHT_EDGE_BACK_NAVIGATION;
+        }
+        Bundle args = new Bundle();
+        args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Nullable
+    private static TutorialFragment getFragmentForTutorialType(TutorialType tutorialType) {
+        switch (tutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+            case LEFT_EDGE_BACK_NAVIGATION:
+            case BACK_NAVIGATION_COMPLETE:
+                return new BackGestureTutorialFragment();
+            case HOME_NAVIGATION:
+            case HOME_NAVIGATION_COMPLETE:
+                return new HomeGestureTutorialFragment();
+            case OVERVIEW_NAVIGATION:
+            case OVERVIEW_NAVIGATION_COMPLETE:
+                return new OverviewGestureTutorialFragment();
+            case ASSISTANT:
+            case ASSISTANT_COMPLETE:
+                return new AssistantGestureTutorialFragment();
+            default:
+                Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
+        }
+        return null;
+    }
+
+    abstract int getHandAnimationResId();
+
+    abstract TutorialController createController(TutorialType type);
+
+    abstract Class<? extends TutorialController> getControllerClass();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
+        mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
+        mNavBarGestureHandler = new NavBarGestureHandler(getContext());
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
+        mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback();
+    }
+
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        super.onCreateView(inflater, container, savedInstanceState);
+
+        mRootView = inflater.inflate(R.layout.gesture_tutorial_fragment, container, false);
+        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
+            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
+            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
+            return insets;
+        });
+        mRootView.setOnTouchListener(this);
+        mHandCoachingAnimation = new TutorialHandAnimation(getContext(), mRootView,
+                getHandAnimationResId());
+        return mRootView;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        changeController(mTutorialType);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mHandCoachingAnimation.stop();
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        // Note: Using logical or to ensure both functions get called.
+        return mEdgeBackGestureHandler.onTouch(view, motionEvent)
+                | mNavBarGestureHandler.onTouch(view, motionEvent);
+    }
+
+    void onAttachedToWindow() {
+        mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
+    }
+
+    void onDetachedFromWindow() {
+        mEdgeBackGestureHandler.setViewGroupParent(null);
+    }
+
+    void changeController(TutorialType tutorialType) {
+        if (getControllerClass().isInstance(mTutorialController)) {
+            mTutorialController.setTutorialType(tutorialType);
+        } else {
+            mTutorialController = createController(tutorialType);
+        }
+        mTutorialController.transitToController();
+        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
+        mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
+        mTutorialType = tutorialType;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    View getRootView() {
+        return mRootView;
+    }
+
+    TutorialHandAnimation getHandAnimation() {
+        return mHandCoachingAnimation;
+    }
+
+    void closeTutorial() {
+        FragmentActivity activity = getActivity();
+        if (activity != null) {
+            activity.finish();
+        }
+    }
+
+    void startSystemNavigationSetting() {
+        startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
new file mode 100644
index 0000000..c810e43
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+import java.time.Duration;
+
+/** Hand coaching animation. */
+final class TutorialHandAnimation {
+
+    // A delay for waiting the Activity fully launches.
+    private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
+
+    private final ImageView mHandCoachingView;
+    private final AnimatedVectorDrawable mGestureAnimation;
+
+    TutorialHandAnimation(Context context, View rootView, int resId) {
+        mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
+        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context, resId);
+    }
+
+    /** [Re]starts animation for the given tutorial. */
+    void startLoopedAnimation(TutorialType tutorialType) {
+        mHandCoachingView.setVisibility(View.VISIBLE);
+        if (mGestureAnimation.isRunning()) {
+            stop();
+        }
+
+        mGestureAnimation.clearAnimationCallbacks();
+        mGestureAnimation.registerAnimationCallback(
+                new Animatable2.AnimationCallback() {
+                    @Override
+                    public void onAnimationEnd(Drawable drawable) {
+                        super.onAnimationEnd(drawable);
+                        mGestureAnimation.start();
+                    }
+                });
+        start(tutorialType);
+    }
+
+    private void start(TutorialType tutorialType) {
+        // Because the gesture animation has only the right side form.
+        // The left side form of the gesture animation is made from flipping the View.
+        float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
+        mHandCoachingView.setRotationY(rotationY);
+        mHandCoachingView.setImageDrawable(mGestureAnimation);
+        mHandCoachingView.postDelayed(mGestureAnimation::start, ANIMATION_START_DELAY.toMillis());
+    }
+
+    void stop() {
+        mGestureAnimation.clearAnimationCallbacks();
+        mGestureAnimation.stop();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 8e5ed1a..eac45e9 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,230 +16,437 @@
 
 package com.android.quickstep.logging;
 
-import static android.stats.launcher.nano.Launcher.ALLAPPS;
-import static android.stats.launcher.nano.Launcher.HOME;
-import static android.stats.launcher.nano.Launcher.LAUNCH_APP;
-import static android.stats.launcher.nano.Launcher.LAUNCH_TASK;
-import static android.stats.launcher.nano.Launcher.DISMISS_TASK;
-import static android.stats.launcher.nano.Launcher.BACKGROUND;
-import static android.stats.launcher.nano.Launcher.OVERVIEW;
+import 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.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;
+import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
+import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
 
 import android.content.Context;
-import android.content.Intent;
-import android.stats.launcher.nano.Launcher;
-import android.stats.launcher.nano.LauncherExtension;
-import android.stats.launcher.nano.LauncherTarget;
 import android.util.Log;
-import android.view.View;
 
-import com.android.launcher3.ItemInfo;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+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.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.util.ComponentKey;
-import com.android.systemui.shared.system.StatsLogCompat;
-import com.google.protobuf.nano.MessageNano;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+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.util.Executors;
+import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.LogConfig;
+import com.android.systemui.shared.system.SysUiStatsLog;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.OptionalInt;
 
 /**
- * This method calls the StatsLog hidden method until they are made available public.
+ * This class calls StatsLog compile time generated methods.
  *
  * To see if the logs are properly sent to statsd, execute following command.
- * $ adb root && adb shell statsd
- * $ adb shell cmd stats print-logs
- * $ adb logcat | grep statsd  OR $ adb logcat -b stats
+ * $ wwdebug (to turn on the logcat printout)
+ * $ wwlogcat (see logcat with grep filter on)
+ * $ statsd_testdrive (see how ww is writing the proto to statsd buffer)
  */
 public class StatsLogCompatManager extends StatsLogManager {
 
-    private static final int SUPPORTED_TARGET_DEPTH = 2;
-    private static final String TAG = "StatsLogCompatManager";
-    private static final boolean DEBUG = false;
+    private static final String TAG = "StatsLog";
+    private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
 
-    public StatsLogCompatManager(Context context) { }
+    private static Context sContext;
 
-    @Override
-    public void logAppLaunch(View v, Intent intent) {
-        LauncherExtension ext = new LauncherExtension();
-        ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
-        int srcState = mStateProvider.getCurrentState();
-        fillInLauncherExtension(v, ext);
-        if (ext.srcTarget[0] != null) {
-            ext.srcTarget[0].item = LauncherTarget.APP_ICON;
-        }
-        StatsLogCompat.write(LAUNCH_APP, srcState, BACKGROUND /* dstState */,
-                MessageNano.toByteArray(ext), true);
+    private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0);
+    // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates
+    // from nano to lite, bake constant to prevent robo test failure.
+    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;
+
+    public StatsLogCompatManager(Context context) {
+        sContext = context;
     }
 
     @Override
-    public void logTaskLaunch(View v, ComponentKey componentKey) {
-        LauncherExtension ext = new LauncherExtension();
-        ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
-        int srcState = OVERVIEW;
-        fillInLauncherExtension(v, ext);
-        StatsLogCompat.write(LAUNCH_TASK, srcState, BACKGROUND /* dstState */,
-                MessageNano.toByteArray(ext), true);
+    public StatsLogger logger() {
+        return new StatsCompatLogger();
     }
 
+    /**
+     * Logs a ranking event and accompanying {@link InstanceId} and package name.
+     */
     @Override
-    public void logTaskDismiss(View v, ComponentKey componentKey) {
-        LauncherExtension ext = new LauncherExtension();
-        ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
-        int srcState = OVERVIEW;
-        fillInLauncherExtension(v, ext);
-        StatsLogCompat.write(DISMISS_TASK, srcState, BACKGROUND /* dstState */,
-                MessageNano.toByteArray(ext), true);
+    public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName,
+            int position) {
+        SysUiStatsLog.write(SysUiStatsLog.RANKING_SELECTED,
+                rankingEvent.getId() /* event_id = 1; */,
+                packageName /* package_name = 2; */,
+                instanceId.getId() /* instance_id = 3; */,
+                position /* position_picked = 4; */);
     }
 
+    /**
+     * Logs the workspace layout information on the model thread.
+     */
     @Override
-    public void logSwipeOnContainer(boolean isSwipingToLeft, int pageId) {
-        LauncherExtension ext = new LauncherExtension();
-        ext.srcTarget = new LauncherTarget[1];
-        int srcState = mStateProvider.getCurrentState();
-        fillInLauncherExtensionWithPageId(ext, pageId);
-        int launcherAction = isSwipingToLeft ? Launcher.SWIPE_LEFT : Launcher.SWIPE_RIGHT;
-        StatsLogCompat.write(launcherAction, srcState, srcState,
-                MessageNano.toByteArray(ext), true);
+    public void logSnapshot() {
+        LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
+                new SnapshotWorker());
     }
 
-    public static boolean fillInLauncherExtension(View v, LauncherExtension extension) {
-        if (DEBUG) {
-            Log.d(TAG, "fillInLauncherExtension");
+    private class SnapshotWorker extends BaseModelUpdateTask {
+        private final InstanceId mInstanceId;
+        SnapshotWorker() {
+            mInstanceId = new InstanceIdSequence(
+                    1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
         }
 
-        StatsLogUtils.LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(v);
-        if (v == null || !(v.getTag() instanceof ItemInfo) || provider == null) {
-            if (DEBUG) {
-                Log.d(TAG, "View or provider is null, or view doesn't have an ItemInfo tag.");
+        @Override
+        public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+            IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
+            ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone();
+            ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone();
+            for (ItemInfo info : workspaceItems) {
+                LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
+                writeSnapshot(atomInfo, mInstanceId);
+            }
+            for (FolderInfo fInfo : folders) {
+                try {
+                    ArrayList<WorkspaceItemInfo> folderContents =
+                            (ArrayList) Executors.MAIN_EXECUTOR.submit(fInfo.contents::clone).get();
+                    for (ItemInfo info : folderContents) {
+                        LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
+                        writeSnapshot(atomInfo, mInstanceId);
+                    }
+                } catch (Exception e) { }
+            }
+            for (ItemInfo info : appWidgets) {
+                LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
+                writeSnapshot(atomInfo, mInstanceId);
+            }
+        }
+    }
+
+    private static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
+        if (IS_VERBOSE) {
+            Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
+        }
+        if (!Utilities.ATLEAST_R) {
+            return;
+        }
+        SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT,
+                LAUNCHER_WORKSPACE_SNAPSHOT.getId() /* event_id */,
+                info.getItemCase().getNumber() /* target_id */,
+                instanceId.getId() /* instance_id */,
+                0 /* uid */,
+                getPackageName(info) /* package_name */,
+                getComponentName(info) /* component_name */,
+                getGridX(info, false) /* grid_x */,
+                getGridY(info, false) /* grid_y */,
+                getPageId(info) /* page_id */,
+                getGridX(info, true) /* grid_x_parent */,
+                getGridY(info, true) /* grid_y_parent */,
+                getParentPageId(info) /* page_id_parent */,
+                getHierarchy(info) /* hierarchy */,
+                info.getIsWork() /* is_work_profile */,
+                info.getAttribute().getNumber() /* origin */,
+                getCardinality(info) /* cardinality */,
+                info.getWidget().getSpanX(),
+                info.getWidget().getSpanY());
+    }
+
+    /**
+     * Helps to construct and write statsd compatible log message.
+     */
+    private static class StatsCompatLogger implements StatsLogger {
+
+        private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo();
+        private ItemInfo mItemInfo = DEFAULT_ITEM_INFO;
+        private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
+        private OptionalInt mRank = OptionalInt.empty();
+        private Optional<ContainerInfo> mContainerInfo = Optional.empty();
+        private int mSrcState = LAUNCHER_STATE_UNSPECIFIED;
+        private int mDstState = LAUNCHER_STATE_UNSPECIFIED;
+        private Optional<FromState> mFromState = Optional.empty();
+        private Optional<ToState> mToState = Optional.empty();
+        private Optional<String> mEditText = Optional.empty();
+
+        @Override
+        public StatsLogger withItemInfo(ItemInfo itemInfo) {
+            if (mContainerInfo.isPresent()) {
+                throw new IllegalArgumentException(
+                        "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
+            }
+            this.mItemInfo = itemInfo;
+            return this;
+        }
+
+        @Override
+        public StatsLogger withInstanceId(InstanceId instanceId) {
+            this.mInstanceId = instanceId;
+            return this;
+        }
+
+        @Override
+        public StatsLogger withRank(int rank) {
+            this.mRank = OptionalInt.of(rank);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withSrcState(int srcState) {
+            this.mSrcState = srcState;
+            return this;
+        }
+
+        @Override
+        public StatsLogger withDstState(int dstState) {
+            this.mDstState = dstState;
+            return this;
+        }
+
+        @Override
+        public StatsLogger withContainerInfo(ContainerInfo containerInfo) {
+            if (mItemInfo != DEFAULT_ITEM_INFO) {
+                throw new IllegalArgumentException(
+                        "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
+            }
+            this.mContainerInfo = Optional.of(containerInfo);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withFromState(FromState fromState) {
+            this.mFromState = Optional.of(fromState);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withToState(ToState toState) {
+            this.mToState = Optional.of(toState);
+            return this;
+        }
+
+        @Override
+        public StatsLogger withEditText(String editText) {
+            this.mEditText = Optional.of(editText);
+            return this;
+        }
+
+        @Override
+        public void log(EventEnum event) {
+            if (!Utilities.ATLEAST_R) {
+                return;
             }
 
-            return false;
+            if (mItemInfo.container < 0) {
+                // Item is not within a folder. Write to StatsLog in same thread.
+                write(event, mInstanceId, applyOverwrites(mItemInfo.buildProto()), mSrcState,
+                        mDstState);
+            } else {
+                // Item is inside the folder, fetch folder info in a BG thread
+                // and then write to StatsLog.
+                LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
+                        new BaseModelUpdateTask() {
+                            @Override
+                            public void execute(LauncherAppState app, BgDataModel dataModel,
+                                    AllAppsList apps) {
+                                FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container);
+                                write(event, mInstanceId,
+                                        applyOverwrites(mItemInfo.buildProto(folderInfo)),
+                                        mSrcState, mDstState);
+                            }
+                        });
+            }
         }
-        ItemInfo itemInfo = (ItemInfo) v.getTag();
-        Target child = new Target();
-        Target parent = new Target();
-        provider.fillInLogContainerData(v, itemInfo, child, parent);
-        extension.srcTarget[0] = new LauncherTarget();
-        extension.srcTarget[1] = new LauncherTarget();
-        copy(child, extension.srcTarget[0]);
-        copy(parent, extension.srcTarget[1]);
-        return true;
+
+        private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) {
+            LauncherAtom.ItemInfo.Builder itemInfoBuilder =
+                    (LauncherAtom.ItemInfo.Builder) atomInfo.toBuilder();
+
+            mRank.ifPresent(itemInfoBuilder::setRank);
+            mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo);
+
+            if (mFromState.isPresent() || mToState.isPresent() || mEditText.isPresent()) {
+                FolderIcon.Builder folderIconBuilder = (FolderIcon.Builder) itemInfoBuilder
+                        .getFolderIcon()
+                        .toBuilder();
+                mFromState.ifPresent(folderIconBuilder::setFromLabelState);
+                mToState.ifPresent(folderIconBuilder::setToLabelState);
+                mEditText.ifPresent(folderIconBuilder::setLabelInfo);
+                itemInfoBuilder.setFolderIcon(folderIconBuilder);
+            }
+            return itemInfoBuilder.build();
+        }
+
+        private void write(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo,
+                int srcState, int dstState) {
+            if (IS_VERBOSE) {
+                String name = (event instanceof Enum) ? ((Enum) event).name() :
+                        event.getId() + "";
+
+                Log.d(TAG, instanceId == DEFAULT_INSTANCE_ID
+                        ? String.format("\n%s (State:%s->%s)\n%s", name, getStateString(srcState),
+                        getStateString(dstState), atomInfo)
+                        : String.format("\n%s (State:%s->%s) (InstanceId:%s)\n%s", name,
+                                getStateString(srcState), getStateString(dstState), instanceId,
+                                atomInfo));
+            }
+
+            SysUiStatsLog.write(
+                    SysUiStatsLog.LAUNCHER_EVENT,
+                    SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
+                    srcState,
+                    dstState,
+                    null /* launcher extensions, deprecated */,
+                    false /* quickstep_enabled, deprecated */,
+                    event.getId() /* event_id */,
+                    atomInfo.getItemCase().getNumber() /* target_id */,
+                    instanceId.getId() /* instance_id TODO */,
+                    0 /* uid TODO */,
+                    getPackageName(atomInfo) /* package_name */,
+                    getComponentName(atomInfo) /* component_name */,
+                    getGridX(atomInfo, false) /* grid_x */,
+                    getGridY(atomInfo, false) /* grid_y */,
+                    getPageId(atomInfo) /* page_id */,
+                    getGridX(atomInfo, true) /* grid_x_parent */,
+                    getGridY(atomInfo, true) /* grid_y_parent */,
+                    getParentPageId(atomInfo) /* page_id_parent */,
+                    getHierarchy(atomInfo) /* hierarchy */,
+                    atomInfo.getIsWork() /* is_work_profile */,
+                    atomInfo.getRank() /* rank */,
+                    atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */,
+                    atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */,
+                    atomInfo.getFolderIcon().getLabelInfo() /* edittext */,
+                    getCardinality(atomInfo) /* cardinality */);
+        }
     }
 
-    public static boolean fillInLauncherExtensionWithPageId(LauncherExtension ext, int pageId) {
-        if (DEBUG) {
-            Log.d(TAG, "fillInLauncherExtensionWithPageId, pageId = " + pageId);
+    private static int getCardinality(LauncherAtom.ItemInfo info) {
+        switch (info.getContainerInfo().getContainerCase()){
+            case PREDICTED_HOTSEAT_CONTAINER:
+                return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
+            case SEARCH_RESULT_CONTAINER:
+                return info.getContainerInfo().getSearchResultContainer().getQueryLength();
+            default:
+                return info.getFolderIcon().getCardinality();
         }
-
-        Target target = new Target();
-        target.pageIndex = pageId;
-        ext.srcTarget[0] = new LauncherTarget();
-        copy(target, ext.srcTarget[0]);
-        return true;
     }
 
-    private static void copy(Target src, LauncherTarget dst) {
-        if (DEBUG) {
-            Log.d(TAG, "copy target information from clearcut Target to LauncherTarget.");
-        }
-
-        // Fill in type
-        switch (src.type) {
-            case Target.Type.ITEM:
-                dst.type = LauncherTarget.ITEM_TYPE;
-                break;
-            case Target.Type.CONTROL:
-                dst.type = LauncherTarget.CONTROL_TYPE;
-                break;
-            case Target.Type.CONTAINER:
-                dst.type = LauncherTarget.CONTAINER_TYPE;
-                break;
+    private static String getPackageName(LauncherAtom.ItemInfo info) {
+        switch (info.getItemCase()) {
+            case APPLICATION:
+                return info.getApplication().getPackageName();
+            case SHORTCUT:
+                return info.getShortcut().getShortcutName();
+            case WIDGET:
+                return info.getWidget().getPackageName();
+            case TASK:
+                return info.getTask().getPackageName();
             default:
-                dst.type = LauncherTarget.NONE;
-                break;
+                return null;
         }
-
-        // Fill in item
-        switch (src.itemType) {
-            case ItemType.APP_ICON:
-                dst.item = LauncherTarget.APP_ICON;
-                break;
-            case ItemType.SHORTCUT:
-                dst.item = LauncherTarget.SHORTCUT;
-                break;
-            case ItemType.WIDGET:
-                dst.item = LauncherTarget.WIDGET;
-                break;
-            case ItemType.FOLDER_ICON:
-                dst.item = LauncherTarget.FOLDER_ICON;
-                break;
-            case ItemType.DEEPSHORTCUT:
-                dst.item = LauncherTarget.DEEPSHORTCUT;
-                break;
-            case ItemType.SEARCHBOX:
-                dst.item = LauncherTarget.SEARCHBOX;
-                break;
-            case ItemType.EDITTEXT:
-                dst.item = LauncherTarget.EDITTEXT;
-                break;
-            case ItemType.NOTIFICATION:
-                dst.item = LauncherTarget.NOTIFICATION;
-                break;
-            case ItemType.TASK:
-                dst.item = LauncherTarget.TASK;
-                break;
-            default:
-                dst.item = LauncherTarget.DEFAULT_ITEM;
-                break;
-        }
-
-        // Fill in container
-        switch (src.containerType) {
-            case ContainerType.HOTSEAT:
-                dst.container = LauncherTarget.HOTSEAT;
-                break;
-            case ContainerType.FOLDER:
-                dst.container = LauncherTarget.FOLDER;
-                break;
-            case ContainerType.PREDICTION:
-                dst.container = LauncherTarget.PREDICTION;
-                break;
-            case ContainerType.SEARCHRESULT:
-                dst.container = LauncherTarget.SEARCHRESULT;
-                break;
-            default:
-                dst.container = LauncherTarget.DEFAULT_CONTAINER;
-                break;
-        }
-
-        // Fill in control
-        switch (src.controlType) {
-            case ControlType.UNINSTALL_TARGET:
-                dst.control = LauncherTarget.UNINSTALL;
-                break;
-            case ControlType.REMOVE_TARGET:
-                dst.control = LauncherTarget.REMOVE;
-                break;
-            default:
-                dst.control = LauncherTarget.DEFAULT_CONTROL;
-                break;
-        }
-
-        // Fill in other fields
-        dst.pageId = src.pageIndex;
-        dst.gridX = src.gridX;
-        dst.gridY = src.gridY;
     }
 
-    @Override
-    public void verify() {
-        if(!(StatsLogUtils.LAUNCHER_STATE_ALLAPPS == ALLAPPS &&
-                StatsLogUtils.LAUNCHER_STATE_BACKGROUND == BACKGROUND &&
-                StatsLogUtils.LAUNCHER_STATE_OVERVIEW == OVERVIEW &&
-                StatsLogUtils.LAUNCHER_STATE_HOME == HOME)) {
-            throw new IllegalStateException(
-                    "StatsLogUtil constants doesn't match enums in launcher.proto");
+    private static String getComponentName(LauncherAtom.ItemInfo info) {
+        switch (info.getItemCase()) {
+            case APPLICATION:
+                return info.getApplication().getComponentName();
+            case SHORTCUT:
+                return info.getShortcut().getShortcutName();
+            case WIDGET:
+                return info.getWidget().getComponentName();
+            case TASK:
+                return info.getTask().getComponentName();
+            default:
+                return null;
+        }
+    }
+
+    private static int getGridX(LauncherAtom.ItemInfo info, boolean parent) {
+        if (info.getContainerInfo().getContainerCase() == FOLDER) {
+            if (parent) {
+                return info.getContainerInfo().getFolder().getWorkspace().getGridX();
+            } else {
+                return info.getContainerInfo().getFolder().getGridX();
+            }
+        } else {
+            return info.getContainerInfo().getWorkspace().getGridX();
+        }
+    }
+
+    private static int getGridY(LauncherAtom.ItemInfo info, boolean parent) {
+        if (info.getContainerInfo().getContainerCase() == FOLDER) {
+            if (parent) {
+                return info.getContainerInfo().getFolder().getWorkspace().getGridY();
+            } else {
+                return info.getContainerInfo().getFolder().getGridY();
+            }
+        } else {
+            return info.getContainerInfo().getWorkspace().getGridY();
+        }
+    }
+
+    private static int getPageId(LauncherAtom.ItemInfo info) {
+        switch (info.getContainerInfo().getContainerCase()) {
+            case FOLDER:
+                return info.getContainerInfo().getFolder().getPageIndex();
+            default:
+                return info.getContainerInfo().getWorkspace().getPageIndex();
+        }
+    }
+
+    private static int getParentPageId(LauncherAtom.ItemInfo info) {
+        switch (info.getContainerInfo().getContainerCase()) {
+            case FOLDER:
+                return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
+            case SEARCH_RESULT_CONTAINER:
+                return info.getContainerInfo().getSearchResultContainer().getWorkspace()
+                        .getPageIndex();
+            default:
+                return info.getContainerInfo().getWorkspace().getPageIndex();
+        }
+    }
+
+    private static int getHierarchy(LauncherAtom.ItemInfo info) {
+        if (info.getContainerInfo().getContainerCase() == FOLDER) {
+            return info.getContainerInfo().getFolder().getParentContainerCase().getNumber()
+                    + FOLDER_HIERARCHY_OFFSET;
+        } else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) {
+            return info.getContainerInfo().getSearchResultContainer().getParentContainerCase()
+                    .getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET;
+        } else {
+            return info.getContainerInfo().getContainerCase().getNumber();
+        }
+    }
+
+    private static String getStateString(int state) {
+        switch (state) {
+            case LAUNCHER_UICHANGED__DST_STATE__BACKGROUND:
+                return "BACKGROUND";
+            case LAUNCHER_UICHANGED__DST_STATE__HOME:
+                return "HOME";
+            case LAUNCHER_UICHANGED__DST_STATE__OVERVIEW:
+                return "OVERVIEW";
+            case LAUNCHER_UICHANGED__DST_STATE__ALLAPPS:
+                return "ALLAPPS";
+            default:
+                return "INVALID";
+
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
new file mode 100644
index 0000000..dfb8c1d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
+
+import java.util.function.BiPredicate;
+
+public class ActivityInitListener<T extends BaseActivity> implements SchedulerCallback<T> {
+
+    private BiPredicate<T, Boolean> mOnInitListener;
+    private final ActivityTracker<T> mActivityTracker;
+
+    private boolean mIsRegistered = false;
+
+    /**
+     * @param onInitListener a callback made when the activity is initialized. The callback should
+     *                       return true to continue receiving callbacks (ie. for if the activity is
+     *                       recreated).
+     */
+    public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
+            ActivityTracker<T> tracker) {
+        mOnInitListener = onInitListener;
+        mActivityTracker = tracker;
+    }
+
+    @Override
+    public final boolean init(T activity, boolean alreadyOnHome) {
+        if (!mIsRegistered) {
+            return false;
+        }
+        return handleInit(activity, alreadyOnHome);
+    }
+
+    protected boolean handleInit(T activity, boolean alreadyOnHome) {
+        return mOnInitListener.test(activity, alreadyOnHome);
+    }
+
+    /**
+     * Registers the activity-created listener. If the activity is already created, then the
+     * callback provided in the constructor will be called synchronously.
+     * @param intent The intent that will be used to initialize the activity, if the activity
+     *               doesn't already exist. We add the callback as an extra on this intent.
+     */
+    public void register(Intent intent) {
+        mIsRegistered = true;
+        mActivityTracker.runCallbackWhenActivityExists(this, intent);
+    }
+
+    /**
+     * After calling this, we won't {@link #init} even when the activity is ready.
+     */
+    public void unregister() {
+        mIsRegistered = false;
+        mOnInitListener = null;
+    }
+
+    /**
+     * Starts the given intent with the provided animation. Unlike {@link #register(Intent)}, this
+     * method will not call {@link #init} if the activity already exists, it will only call it when
+     * we get handleIntent() for the provided intent that we're starting.
+     */
+    public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
+            Context context, Handler handler, long duration) {
+        mIsRegistered = true;
+
+        Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
+        context.startActivity(addToIntent(new Intent(intent)), options);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/BinderTracker.java b/quickstep/src/com/android/quickstep/util/BinderTracker.java
index 32d0d53..cb04e5b 100644
--- a/quickstep/src/com/android/quickstep/util/BinderTracker.java
+++ b/quickstep/src/com/android/quickstep/util/BinderTracker.java
@@ -31,7 +31,7 @@
     private static final String TAG = "BinderTracker";
 
     public static void start() {
-        if (!FeatureFlags.IS_DOGFOOD_BUILD) {
+        if (!FeatureFlags.IS_STUDIO_BUILD) {
             Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
             return;
         }
@@ -40,7 +40,7 @@
     }
 
     public static void stop() {
-        if (!FeatureFlags.IS_DOGFOOD_BUILD) {
+        if (!FeatureFlags.IS_STUDIO_BUILD) {
             Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
             return;
         }
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
new file mode 100644
index 0000000..e998e9a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.Picture;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.core.content.FileProvider;
+
+import com.android.launcher3.BuildConfig;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.utilities.BitmapUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+/**
+ * Utility class containing methods to help manage image actions such as sharing, cropping, and
+ * saving image.
+ */
+public class ImageActionUtils {
+
+    private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".overview.fileprovider";
+    private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/;
+    private static final String SUB_FOLDER = "Overview";
+    private static final String BASE_NAME = "overview_image_";
+
+    /**
+     * Saves screenshot to location determine by SystemUiProxy
+     */
+    public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot,
+            Rect screenshotBounds,
+            Insets visibleInsets, Task.TaskKey task) {
+        systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot),
+                screenshotBounds, visibleInsets, task);
+    }
+
+    /**
+     * Launch the activity to share image.
+     */
+    @UiThread
+    public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier,
+            Rect crop, Intent intent, String tag) {
+        if (bitmapSupplier.get() == null) {
+            Log.e(tag, "No snapshot available, not starting share.");
+            return;
+        }
+
+        UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context,
+                bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri,
+                tag));
+    }
+
+    /**
+     * Starts activity based on given intent created from image uri.
+     */
+    @WorkerThread
+    public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop,
+            Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) {
+        Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent);
+
+        // Work around b/159412574
+        if (intents.length == 1) {
+            context.startActivity(intents[0]);
+        } else {
+            context.startActivities(intents);
+        }
+    }
+
+    /**
+     * Converts image bitmap to Uri by temporarily saving bitmap to cache, and creating Uri pointing
+     * to that location. Used to be able to share an image with another app.
+     *
+     * @param bitmap  The whole bitmap to be shared.
+     * @param crop    The section of the bitmap to be shared.
+     * @param context The application context, used to interact with file system.
+     * @param tag     Tag used to log errors.
+     * @return Uri that points to the cropped version of desired bitmap to share.
+     */
+    @WorkerThread
+    public static Uri getImageUri(Bitmap bitmap, Rect crop, Context context, String tag) {
+        clearOldCacheFiles(context);
+        Bitmap croppedBitmap = cropBitmap(bitmap, crop);
+        int cropHash = crop == null ? 0 : crop.hashCode();
+        String baseName = BASE_NAME + bitmap.hashCode() + "_" + cropHash + ".png";
+        File parent = new File(context.getCacheDir(), SUB_FOLDER);
+        parent.mkdir();
+        File file = new File(parent, baseName);
+
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
+        } catch (IOException e) {
+            Log.e(tag, "Error saving image", e);
+        }
+
+        return FileProvider.getUriForFile(context, AUTHORITY, file);
+    }
+
+    /**
+     * Crops the bitmap to the provided size and returns a software backed bitmap whenever possible.
+     *
+     * @param bitmap The bitmap to be cropped.
+     * @param crop   The section of the bitmap in the crop.
+     * @return The cropped bitmap.
+     */
+    @WorkerThread
+    public static Bitmap cropBitmap(Bitmap bitmap, Rect crop) {
+        Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+        if (crop == null) {
+            crop = new Rect(src);
+        }
+        if (crop.equals(src)) {
+            return bitmap;
+        } else {
+            if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
+                return Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(),
+                        crop.height());
+            }
+
+            // For hardware bitmaps, use the Picture API to directly create a software bitmap
+            Picture picture = new Picture();
+            Canvas canvas = picture.beginRecording(crop.width(), crop.height());
+            canvas.drawBitmap(bitmap, -crop.left, -crop.top, null);
+            picture.endRecording();
+            return Bitmap.createBitmap(picture, crop.width(), crop.height(),
+                    Bitmap.Config.ARGB_8888);
+        }
+    }
+
+    /**
+     * Gets the intent used to share image.
+     */
+    @WorkerThread
+    private static Intent[] getShareIntentForImageUri(Uri uri, Intent intent) {
+        if (intent == null) {
+            intent = new Intent();
+        }
+        ClipData clipdata = new ClipData(new ClipDescription("content",
+                new String[]{"image/png"}),
+                new ClipData.Item(uri));
+        intent.setAction(Intent.ACTION_SEND)
+                .setComponent(null)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+                .setType("image/png")
+                .putExtra(Intent.EXTRA_STREAM, uri)
+                .setClipData(clipdata);
+        return new Intent[]{Intent.createChooser(intent, null).addFlags(FLAG_ACTIVITY_NEW_TASK)};
+    }
+
+    private static void clearOldCacheFiles(Context context) {
+        THREAD_POOL_EXECUTOR.execute(() -> {
+            File parent = new File(context.getCacheDir(), SUB_FOLDER);
+            File[] files = parent.listFiles((File f, String s) -> s.startsWith(BASE_NAME));
+            if (files != null) {
+                for (File file: files) {
+                    if (file.lastModified() + FILE_LIFE < System.currentTimeMillis()) {
+                        file.delete();
+                    }
+                }
+            }
+        });
+
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 2e118b4..ae19d73 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -15,30 +15,22 @@
  */
 package com.android.quickstep.util;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Rect;
-
-import androidx.annotation.AnyThread;
-import androidx.annotation.IntDef;
+import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
 
-import java.lang.annotation.Retention;
-
 public class LayoutUtils {
 
-    private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1;
-    private static final int MULTI_WINDOW_STRATEGY_DEVICE_PROFILE = 2;
-
-    @Retention(SOURCE)
-    @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE})
-    private @interface MultiWindowStrategy {}
-
     /**
      * The height for the swipe up motion
      */
@@ -50,89 +42,32 @@
         return swipeHeight;
     }
 
-    public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        float extraSpace;
-        if (dp.isVerticalBarLayout()) {
-            extraSpace = 0;
-        } else {
-            Resources res = context.getResources();
-
-            extraSpace = getDefaultSwipeHeight(context, dp) + dp.verticalDragHandleSizePx
-                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
-                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
-        }
-        calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect);
-    }
-
-    public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect);
-    }
-
-    @AnyThread
-    public static void calculateTaskSize(Context context, DeviceProfile dp,
-            float extraVerticalSpace, @MultiWindowStrategy int multiWindowStrategy, Rect outRect) {
-        float taskWidth, taskHeight, paddingHorz;
-        Resources res = context.getResources();
-        Rect insets = dp.getInsets();
-
-        if (dp.isMultiWindowMode) {
-            if (multiWindowStrategy == MULTI_WINDOW_STRATEGY_HALF_SCREEN) {
-                DeviceProfile fullDp = dp.getFullScreenProfile();
-                // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
-                // account for system insets
-                taskWidth = fullDp.availableWidthPx;
-                taskHeight = fullDp.availableHeightPx;
-                float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size)
-                        / 2;
-
-                if (fullDp.isLandscape) {
-                    taskWidth = taskWidth / 2 - halfDividerSize;
-                } else {
-                    taskHeight = taskHeight / 2 - halfDividerSize;
-                }
-            } else {
-                // multiWindowStrategy == MULTI_WINDOW_STRATEGY_DEVICE_PROFILE
-                taskWidth = dp.widthPx;
-                taskHeight = dp.heightPx;
-            }
-            paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space);
-        } else {
-            taskWidth = dp.availableWidthPx;
-            taskHeight = dp.availableHeightPx;
-            paddingHorz = res.getDimension(dp.isVerticalBarLayout()
-                    ? R.dimen.landscape_task_card_horz_space
-                    : R.dimen.portrait_task_card_horz_space);
-        }
-
-        float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
-        float paddingVert = res.getDimension(R.dimen.task_card_vert_space);
-
-        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
-        // we override the insets ourselves.
-        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
-        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
-
-        float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert;
-        float availableWidth = launcherVisibleWidth - paddingHorz;
-
-        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
-        float outWidth = scale * taskWidth;
-        float outHeight = scale * taskHeight;
-
-        // Center in the visible space
-        float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
-        float y = insets.top + Math.max(topIconMargin,
-                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
-        outRect.set(Math.round(x), Math.round(y),
-                Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
-    }
-
-    public static int getShelfTrackingDistance(Context context, DeviceProfile dp) {
+    public static int getShelfTrackingDistance(Context context, DeviceProfile dp,
+            PagedOrientationHandler orientationHandler) {
         // Track the bottom of the window.
+        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
+            Rect taskSize = new Rect();
+            LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
+                    orientationHandler);
+            return (dp.heightPx - taskSize.height()) / 2;
+        }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
         int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
                 R.dimen.task_card_vert_space);
         return shelfHeight + spaceBetweenShelfAndRecents;
     }
+
+    /**
+     * Recursively sets view and all children enabled/disabled.
+     * @param view Top most parent view to change.
+     * @param enabled True = enable, False = disable.
+     */
+    public static void setViewEnabled(View view, boolean enabled) {
+        view.setEnabled(enabled);
+        if (view instanceof ViewGroup) {
+            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
+                setViewEnabled(((ViewGroup) view).getChildAt(i), enabled);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 801a560..a5d4568 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -15,13 +15,17 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_LSQ_VELOCITY_PROVIDER;
+
 import android.content.Context;
 import android.content.res.Resources;
+import android.util.Log;
 import android.view.MotionEvent;
 
 import com.android.launcher3.Alarm;
 import com.android.launcher3.R;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.testing.TestProtocol;
 
 /**
  * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
@@ -37,8 +41,7 @@
     private static final long FORCE_PAUSE_TIMEOUT = 300;
 
     /**
-     * After {@link #makePauseHarderToTrigger()}, must
-     * move slowly for this long to trigger a pause.
+     * After {@link #mMakePauseHarderToTrigger}, must move slowly for this long to trigger a pause.
      */
     private static final long HARDER_TRIGGER_TIMEOUT = 400;
 
@@ -49,13 +52,10 @@
     private final Alarm mForcePauseTimeout;
     private final boolean mMakePauseHarderToTrigger;
     private final Context mContext;
+    private final VelocityProvider mVelocityProvider;
 
-    private Long mPreviousTime = null;
-    private Float mPreviousPosition = null;
     private Float mPreviousVelocity = null;
 
-    private Float mFirstPosition = null;
-
     private OnMotionPauseListener mOnMotionPauseListener;
     private boolean mIsPaused;
     // Bias more for the first pause to make it feel extra responsive.
@@ -73,15 +73,27 @@
      * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
      */
     public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger) {
+        this(context, makePauseHarderToTrigger, MotionEvent.AXIS_Y);
+    }
+
+    /**
+     * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
+     */
+    public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis) {
         mContext = context;
         Resources res = context.getResources();
         mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "creating alarm");
+        }
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
+        mVelocityProvider = ENABLE_LSQ_VELOCITY_PROVIDER.get()
+                ? new LSqVelocityProvider(axis) : new LinearVelocityProvider(axis);
     }
 
     /**
@@ -101,28 +113,29 @@
 
     /**
      * Computes velocity and acceleration to determine whether the motion is paused.
-     * @param position The x or y component of the motion being tracked.
-     *
-     * TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
+     * @param ev The motion being tracked.
      */
-    public void addPosition(float position, long time) {
-        if (mFirstPosition == null) {
-            mFirstPosition = position;
+    public void addPosition(MotionEvent ev) {
+        addPosition(ev, 0);
+    }
+
+    /**
+     * Computes velocity and acceleration to determine whether the motion is paused.
+     * @param ev The motion being tracked.
+     * @param pointerIndex Index for the pointer being tracked in the motion event
+     */
+    public void addPosition(MotionEvent ev, int pointerIndex) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "setting alarm");
         }
         mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
                 ? HARDER_TRIGGER_TIMEOUT
                 : FORCE_PAUSE_TIMEOUT);
-        if (mPreviousTime != null && mPreviousPosition != null) {
-            long changeInTime = Math.max(1, time - mPreviousTime);
-            float changeInPosition = position - mPreviousPosition;
-            float velocity = changeInPosition / changeInTime;
-            if (mPreviousVelocity != null) {
-                checkMotionPaused(velocity, mPreviousVelocity, time);
-            }
-            mPreviousVelocity = velocity;
+        Float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
+        if (newVelocity != null && mPreviousVelocity != null) {
+            checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
         }
-        mPreviousTime = time;
-        mPreviousPosition = position;
+        mPreviousVelocity = newVelocity;
     }
 
     private void checkMotionPaused(float velocity, float prevVelocity, long time) {
@@ -162,6 +175,9 @@
     }
 
     private void updatePaused(boolean isPaused) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "updatePaused: " + isPaused);
+        }
         if (mDisallowPause) {
             isPaused = false;
         }
@@ -178,13 +194,14 @@
     }
 
     public void clear() {
-        mPreviousTime = null;
-        mPreviousPosition = null;
+        mVelocityProvider.clear();
         mPreviousVelocity = null;
-        mFirstPosition = null;
         setOnMotionPauseListener(null);
         mIsPaused = mHasEverBeenPaused = false;
         mSlowStartTime = 0;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "canceling alarm");
+        }
         mForcePauseTimeout.cancelAlarm();
     }
 
@@ -195,4 +212,188 @@
     public interface OnMotionPauseListener {
         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;
+        }
+    }
+
+    /**
+     * Java implementation of {@link android.view.VelocityTracker} using the Least Square (deg 2)
+     * algorithm.
+     */
+    private static class LSqVelocityProvider implements VelocityProvider {
+
+        // Maximum age of a motion event to be considered when calculating the velocity.
+        private static final long HORIZON_MS = 100;
+        // Number of samples to keep.
+        private static final int HISTORY_SIZE = 20;
+
+        // Position history are stored in a circular array
+        private final float[] mHistoricTimes = new float[HISTORY_SIZE];
+        private final float[] mHistoricPos = new float[HISTORY_SIZE];
+        private int mHistoryCount = 0;
+        private int mHistoryStart = 0;
+
+        private final int mAxis;
+
+        LSqVelocityProvider(int axis) {
+            mAxis = axis;
+        }
+
+        @Override
+        public void clear() {
+            mHistoryCount = mHistoryStart = 0;
+        }
+
+        private void addPositionAndTime(float eventTime, float eventPosition) {
+            mHistoricTimes[mHistoryStart] = eventTime;
+            mHistoricPos[mHistoryStart] = eventPosition;
+            mHistoryStart++;
+            if (mHistoryStart >= HISTORY_SIZE) {
+                mHistoryStart = 0;
+            }
+            mHistoryCount = Math.min(HISTORY_SIZE, mHistoryCount + 1);
+        }
+
+        @Override
+        public Float addMotionEvent(MotionEvent ev, int pointer) {
+            // Add all historic points
+            int historyCount = ev.getHistorySize();
+            for (int i = 0; i < historyCount; i++) {
+                addPositionAndTime(
+                        ev.getHistoricalEventTime(i), ev.getHistoricalAxisValue(mAxis, pointer, i));
+            }
+
+            // Start index for the last position (about to be added)
+            int eventStartIndex = mHistoryStart;
+            addPositionAndTime(ev.getEventTime(), ev.getAxisValue(mAxis, pointer));
+            return solveUnweightedLeastSquaresDeg2(eventStartIndex);
+        }
+
+        /**
+         * Solves the instantaneous velocity.
+         * Based on solveUnweightedLeastSquaresDeg2 in VelocityTracker.cpp
+         */
+        private Float solveUnweightedLeastSquaresDeg2(final int pointPos) {
+            final float eventTime = mHistoricTimes[pointPos];
+
+            float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
+            int count = 0;
+            for (int i = 0; i < mHistoryCount; i++) {
+                int index = pointPos - i;
+                if (index < 0) {
+                    index += HISTORY_SIZE;
+                }
+
+                float time = mHistoricTimes[index];
+                float age = eventTime - time;
+                if (age > HORIZON_MS) {
+                    break;
+                }
+                count++;
+                float xi = -age;
+
+                float yi = mHistoricPos[index];
+                float xi2 = xi * xi;
+                float xi3 = xi2 * xi;
+                float xi4 = xi3 * xi;
+                float xiyi = xi * yi;
+                float xi2yi = xi2 * yi;
+
+                sxi += xi;
+                sxi2 += xi2;
+                sxiyi += xiyi;
+                sxi2yi += xi2yi;
+                syi += yi;
+                sxi3 += xi3;
+                sxi4 += xi4;
+            }
+
+            if (count < 3) {
+                // Too few samples
+                if (count == 2) {
+                    int endPos = pointPos - 1;
+                    if (endPos < 0) {
+                        endPos += HISTORY_SIZE;
+                    }
+                    float denominator = eventTime - mHistoricTimes[endPos];
+                    if (denominator != 0) {
+                        return (eventTime - mHistoricPos[endPos]) / denominator;
+
+                    }
+                }
+                return null;
+            }
+
+            float Sxx = sxi2 - sxi * sxi / count;
+            float Sxy = sxiyi - sxi * syi / count;
+            float Sxx2 = sxi3 - sxi * sxi2 / count;
+            float Sx2y = sxi2yi - sxi2 * syi / count;
+            float Sx2x2 = sxi4 - sxi2 * sxi2 / count;
+
+            float denominator = Sxx * Sx2x2 - Sxx2 * Sxx2;
+            if (denominator == 0) {
+                // division by 0 when computing velocity
+                return null;
+            }
+            // Compute a
+            // float numerator = Sx2y*Sxx - Sxy*Sxx2;
+
+            // Compute b
+            float numerator = Sxy * Sx2x2 - Sx2y * Sxx2;
+            float b = numerator / denominator;
+
+            // Compute c
+            // float c = syi/count - b * sxi/count - a * sxi2/count;
+
+            return b;
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
similarity index 65%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
rename to quickstep/src/com/android/quickstep/util/NavBarPosition.java
index bbb318a..0a98e1b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -15,28 +15,29 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_LANDSCAPE;
-import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_SEASCAPE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 
-import android.content.Context;
 import android.view.Surface;
 
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.SysUINavigationMode;
 
 /**
- * Utility class to check nav bar position
+ * Utility class to check nav bar position.
  */
 public class NavBarPosition {
 
     private final SysUINavigationMode.Mode mMode;
     private final int mDisplayRotation;
 
-    public NavBarPosition(Context context) {
-        mMode = SysUINavigationMode.getMode(context);
-        mDisplayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
+    public NavBarPosition(SysUINavigationMode.Mode mode, DefaultDisplay.Info info) {
+        mMode = mode;
+        mDisplayRotation = info.rotation;
+    }
+
+    public NavBarPosition(SysUINavigationMode.Mode mode, int displayRotation) {
+        mMode = mode;
+        mDisplayRotation = displayRotation;
     }
 
     public boolean isRightEdge() {
@@ -47,8 +48,7 @@
         return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
     }
 
-    public RotationMode getRotationMode() {
-        return isLeftEdge() ? ROTATION_SEASCAPE
-                : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
+    public float getRotation() {
+        return isLeftEdge() ? 90 : (isRightEdge() ? -90 : 0);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
new file mode 100644
index 0000000..9ed2bbe
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.getOpenView;
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
+import android.content.SharedPreferences;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.views.AllAppsEduView;
+
+/**
+ * Extends {@link OnboardingPrefs} for quickstep-specific onboarding data.
+ */
+public class QuickstepOnboardingPrefs extends OnboardingPrefs<BaseQuickstepLauncher> {
+
+    public QuickstepOnboardingPrefs(BaseQuickstepLauncher launcher, SharedPreferences sharedPrefs) {
+        super(launcher, sharedPrefs);
+
+        StateManager<LauncherState> stateManager = launcher.getStateManager();
+        if (!getBoolean(HOME_BOUNCE_SEEN)) {
+            stateManager.addStateListener(new StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE
+                            .get(mLauncher).getMode().hasGestures;
+                    LauncherState prevState = stateManager.getLastState();
+
+                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
+                            && finalState == ALL_APPS && prevState == NORMAL) ||
+                            hasReachedMaxCount(HOME_BOUNCE_COUNT))) {
+                        mSharedPrefs.edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
+                        stateManager.removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        boolean shelfBounceSeen = getBoolean(SHELF_BOUNCE_SEEN);
+        if (!shelfBounceSeen && ENABLE_OVERVIEW_ACTIONS.get()
+                && removeShelfFromOverview(launcher)) {
+            // There's no shelf in overview, so don't bounce it (can't get to all apps anyway).
+            shelfBounceSeen = true;
+            mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, shelfBounceSeen).apply();
+        }
+        if (!shelfBounceSeen) {
+            stateManager.addStateListener(new StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    LauncherState prevState = stateManager.getLastState();
+
+                    if ((finalState == ALL_APPS && prevState == OVERVIEW) ||
+                            hasReachedMaxCount(SHELF_BOUNCE_COUNT)) {
+                        mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
+                        stateManager.removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        if (!hasReachedMaxCount(ALL_APPS_COUNT)) {
+            stateManager.addStateListener(new StateListener<LauncherState>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if (finalState == ALL_APPS) {
+                        if (incrementEventCount(ALL_APPS_COUNT)) {
+                            stateManager.removeStateListener(this);
+                            mLauncher.getScrimView().updateDragHandleVisibility();
+                        }
+                    }
+                }
+            });
+        }
+
+        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
+                HOTSEAT_DISCOVERY_TIP_COUNT)) {
+            stateManager.addStateListener(new StateListener<LauncherState>() {
+                boolean mFromAllApps = false;
+
+                @Override
+                public void onStateTransitionStart(LauncherState toState) {
+                    mFromAllApps = mLauncher.getStateManager().getCurrentStableState() == ALL_APPS;
+                }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    HotseatPredictionController client = mLauncher.getHotseatPredictionController();
+                    if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) {
+                        if (incrementEventCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
+                            client.showEdu();
+                            stateManager.removeStateListener(this);
+                        }
+                    }
+                }
+            });
+        }
+
+        if (SysUINavigationMode.getMode(launcher) == NO_BUTTON
+                && FeatureFlags.ENABLE_ALL_APPS_EDU.get()) {
+            stateManager.addStateListener(new StateListener<LauncherState>() {
+                private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3;
+
+                // Counts the number of consecutive swipes on nav bar without moving screens.
+                private int mCount = 0;
+                private boolean mShouldIncreaseCount;
+
+                @Override
+                public void onStateTransitionStart(LauncherState toState) {
+                    if (toState == NORMAL) {
+                        return;
+                    }
+                    mShouldIncreaseCount = toState == HINT_STATE
+                            && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE;
+                }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if (finalState == NORMAL) {
+                        if (mCount >= MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
+                            if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) {
+                                AllAppsEduView.show(launcher);
+                            }
+                            mCount = 0;
+                        }
+                        return;
+                    }
+
+                    if (mShouldIncreaseCount && finalState == HINT_STATE) {
+                        mCount++;
+                    } else {
+                        mCount = 0;
+                    }
+
+                    if (finalState == ALL_APPS) {
+                        AllAppsEduView view = getOpenView(mLauncher, TYPE_ALL_APPS_EDU);
+                        if (view != null) {
+                            view.close(false);
+                        }
+                    }
+                }
+            });
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
new file mode 100644
index 0000000..d822b6c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
+import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
+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.logging.LoggerUtils.extractObjectNameAndAddress;
+import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+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;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.systemui.shared.system.ConfigurationCompat;
+
+import java.lang.annotation.Retention;
+import java.util.function.IntConsumer;
+
+/**
+ * Container to hold orientation/rotation related information for Launcher.
+ * This is not meant to be an abstraction layer for applying different functionality between
+ * the different orientation/rotations. For that see {@link PagedOrientationHandler}
+ *
+ * This class has initial default state assuming the device and foreground app have
+ * no ({@link Surface#ROTATION_0} rotation.
+ */
+public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+    private static final String TAG = "RecentsOrientedState";
+    private static final boolean DEBUG = true;
+
+    private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateAutoRotateSetting();
+        }
+    };
+    @Retention(SOURCE)
+    @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
+    public @interface SurfaceRotation {}
+
+    private PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+
+    private @SurfaceRotation int mTouchRotation = ROTATION_0;
+    private @SurfaceRotation int mDisplayRotation = ROTATION_0;
+    private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
+
+    // Launcher activity supports multiple orientation, but fallback activity does not
+    private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
+    // Multiple orientation is only supported if density is < 600
+    private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY = 1 << 1;
+    // Shared prefs for rotation, only if activity supports it
+    private static final int FLAG_HOME_ROTATION_ALLOWED_IN_PREFS = 1 << 2;
+    // If the user has enabled system rotation
+    private static final int FLAG_SYSTEM_ROTATION_ALLOWED = 1 << 3;
+    // Multiple orientation is not supported in multiwindow mode
+    private static final int FLAG_MULTIWINDOW_ROTATION_ALLOWED = 1 << 4;
+    // Whether to rotation sensor is supported on the device
+    private static final int FLAG_ROTATION_WATCHER_SUPPORTED = 1 << 5;
+    // Whether to enable rotation watcher when multi-rotation is supported
+    private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6;
+    // Enable home rotation for UI tests, ignoring home rotation value from prefs
+    private static final int FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING = 1 << 7;
+    // Whether the swipe gesture is running, so the recents would stay locked in the
+    // current orientation
+    private static final int FLAG_SWIPE_UP_NOT_RUNNING = 1 << 8;
+
+    private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE =
+            FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY
+            | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
+
+    // State for which rotation watcher will be enabled. We skip it when home rotation or
+    // multi-window is enabled as in that case, activity itself rotates.
+    private static final int VALUE_ROTATION_WATCHER_ENABLED =
+            MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED
+                    | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED
+                    | FLAG_SWIPE_UP_NOT_RUNNING;
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final SharedPreferences mSharedPrefs;
+    private final OrientationEventListener mOrientationListener;
+
+    private final Matrix mTmpMatrix = new Matrix();
+
+    private int mFlags;
+    private int mPreviousRotation = ROTATION_0;
+
+    @Nullable private Configuration mActivityConfiguration;
+
+    /**
+     * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
+     *                              is enabled
+     * @see #setRotationWatcherEnabled(boolean)
+     */
+    public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
+            IntConsumer rotationChangeListener) {
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mSharedPrefs = Utilities.getPrefs(context);
+        mOrientationListener = new OrientationEventListener(context) {
+            @Override
+            public void onOrientationChanged(int degrees) {
+                int newRotation = getRotationForUserDegreesRotated(degrees, mPreviousRotation);
+                if (newRotation != mPreviousRotation) {
+                    mPreviousRotation = newRotation;
+                    rotationChangeListener.accept(newRotation);
+                }
+            }
+        };
+
+        mFlags = sizeStrategy.rotationSupportedByActivity
+                ? FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY : 0;
+
+        Resources res = context.getResources();
+        int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
+                * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
+        if (originalSmallestWidth < 600) {
+            mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
+        }
+        mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
+        initFlags();
+    }
+
+    /**
+     * Sets the configuration for the recents activity, which could affect the activity's rotation
+     * @see #update(int, int)
+     */
+    public boolean setActivityConfiguration(Configuration activityConfiguration) {
+        mActivityConfiguration = activityConfiguration;
+        return update(mTouchRotation, mDisplayRotation);
+    }
+
+    /**
+     * Sets if the host is in multi-window mode
+     */
+    public void setMultiWindowMode(boolean isMultiWindow) {
+        setFlag(FLAG_MULTIWINDOW_ROTATION_ALLOWED, isMultiWindow);
+    }
+
+    /**
+     * Sets if the swipe up gesture is currently running or not
+     */
+    public boolean setGestureActive(boolean isGestureActive) {
+        setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive);
+        return update(mTouchRotation, mDisplayRotation);
+    }
+
+    /**
+     * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler}
+     * @param touchRotation The rotation the nav bar region that is touched is in
+     * @param displayRotation Rotation of the display/device
+     *
+     * @return true if there was any change in the internal state as a result of this call,
+     *         false otherwise
+     */
+    public boolean update(
+            @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) {
+        mRecentsActivityRotation = inferRecentsActivityRotation(displayRotation);
+        mDisplayRotation = displayRotation;
+        mTouchRotation = touchRotation;
+        mPreviousRotation = touchRotation;
+
+        PagedOrientationHandler oldHandler = mOrientationHandler;
+        if (mRecentsActivityRotation == mTouchRotation
+                || (canRecentsActivityRotate() && (mFlags & FLAG_SWIPE_UP_NOT_RUNNING) != 0)) {
+            mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+            if (DEBUG) {
+                Log.d(TAG, "current RecentsOrientedState: " + this);
+            }
+        } else if (mTouchRotation == ROTATION_90) {
+            mOrientationHandler = PagedOrientationHandler.LANDSCAPE;
+        } else if (mTouchRotation == ROTATION_270) {
+            mOrientationHandler = PagedOrientationHandler.SEASCAPE;
+        } else {
+            mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "current RecentsOrientedState: " + this);
+        }
+        return oldHandler != mOrientationHandler;
+    }
+
+    @SurfaceRotation
+    private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
+        if (isRecentsActivityRotationAllowed()) {
+            return mActivityConfiguration == null
+                    ? displayRotation
+                    : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
+        } else {
+            return ROTATION_0;
+        }
+    }
+
+    private void setFlag(int mask, boolean enabled) {
+        boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation
+                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
+                && !canRecentsActivityRotate();
+        if (enabled) {
+            mFlags |= mask;
+        } else {
+            mFlags &= ~mask;
+        }
+
+        boolean isRotationEnabled = !TestProtocol.sDisableSensorRotation
+                && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
+                && !canRecentsActivityRotate();
+        if (wasRotationEnabled != isRotationEnabled) {
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (isRotationEnabled) {
+                    mOrientationListener.enable();
+                } else {
+                    mOrientationListener.disable();
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+        if (ALLOW_ROTATION_PREFERENCE_KEY.equals(s)) {
+            updateHomeRotationSetting();
+        }
+    }
+
+    private void updateAutoRotateSetting() {
+        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
+                Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
+    }
+
+    private void updateHomeRotationSetting() {
+        setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS,
+                mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false));
+    }
+
+    private void initFlags() {
+        SysUINavigationMode.Mode currentMode = SysUINavigationMode.getMode(mContext);
+        boolean rotationWatcherSupported = mOrientationListener.canDetectOrientation() &&
+                currentMode != TWO_BUTTONS;
+        setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, rotationWatcherSupported);
+
+        // initialize external flags
+        updateAutoRotateSetting();
+        updateHomeRotationSetting();
+    }
+
+    /**
+     * Initializes any system values and registers corresponding change listeners. It must be
+     * paired with {@link #destroyListeners()} call
+     */
+    public void initListeners() {
+        if (isMultipleOrientationSupportedByDevice()) {
+            mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
+                    false, mSystemAutoRotateObserver);
+        }
+        initFlags();
+    }
+
+    /**
+     * Unregisters any previously registered listeners.
+     */
+    public void destroyListeners() {
+        if (isMultipleOrientationSupportedByDevice()) {
+            mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
+            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+        }
+        setRotationWatcherEnabled(false);
+    }
+
+    public void forceAllowRotationForTesting(boolean forceAllow) {
+        setFlag(FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING, forceAllow);
+    }
+
+    @SurfaceRotation
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    @SurfaceRotation
+    public int getTouchRotation() {
+        return mTouchRotation;
+    }
+
+    @SurfaceRotation
+    public int getRecentsActivityRotation() {
+        return mRecentsActivityRotation;
+    }
+
+    public boolean isMultipleOrientationSupportedByDevice() {
+        return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE;
+    }
+
+    public boolean isRecentsActivityRotationAllowed() {
+        // Activity rotation is allowed if the multi-simulated-rotation is not supported
+        // (fallback recents or tablets) or activity rotation is enabled by various settings.
+        return ((mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                != MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                || (mFlags & (FLAG_HOME_ROTATION_ALLOWED_IN_PREFS
+                        | FLAG_MULTIWINDOW_ROTATION_ALLOWED
+                        | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0;
+    }
+
+    /**
+     * Returns true if the activity can rotate, if allowed by system rotation settings
+     */
+    public boolean canRecentsActivityRotate() {
+        return (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0 && isRecentsActivityRotationAllowed();
+    }
+
+    /**
+     * Enables or disables the rotation watcher for listening to rotation callbacks
+     */
+    public void setRotationWatcherEnabled(boolean isEnabled) {
+        setFlag(FLAG_ROTATION_WATCHER_ENABLED, isEnabled);
+    }
+
+    /**
+     * Returns the scale and pivot so that the provided taskRect can fit the provided full size
+     */
+    public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
+        Rect insets = dp.getInsets();
+        float fullWidth = dp.widthPx - insets.left - insets.right;
+        float fullHeight = dp.heightPx - insets.top - insets.bottom;
+
+        if (dp.isMultiWindowMode) {
+            WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext);
+            outPivot.set(bounds.availableSize.x, bounds.availableSize.y);
+        } else {
+            outPivot.set(fullWidth, fullHeight);
+        }
+        float scale = Math.min(outPivot.x / taskView.width(), outPivot.y / taskView.height());
+        // We also scale the preview as part of fullScreenParams, so account for that as well.
+        if (fullWidth > 0) {
+            scale = scale * dp.widthPx / fullWidth;
+        }
+
+        if (scale == 1) {
+            outPivot.set(fullWidth / 2, fullHeight / 2);
+        } else if (dp.isMultiWindowMode) {
+            float denominator = 1 / (scale - 1);
+            // Ensure that the task aligns to right bottom for the root view
+            float y = (scale * taskView.bottom - fullHeight) * denominator;
+            float x = (scale * taskView.right - fullWidth) * denominator;
+            outPivot.set(x, y);
+        } else {
+            float factor = scale / (scale - 1);
+            outPivot.set(taskView.left * factor, taskView.top * factor);
+        }
+        return scale;
+    }
+
+    public PagedOrientationHandler getOrientationHandler() {
+        return mOrientationHandler;
+    }
+
+    /**
+     * For landscape, since the navbar is already in a vertical position, we don't have to do any
+     * rotations as the change in Y coordinate is what is read. We only flip the sign of the
+     * y coordinate to make it match existing behavior of swipe to the top to go previous
+     */
+    public void flipVertical(MotionEvent ev) {
+        mTmpMatrix.setScale(1, -1);
+        ev.transform(mTmpMatrix);
+    }
+
+    /**
+     * Creates a matrix to transform the given motion event specified by degrees.
+     * If inverse is {@code true}, the inverse of that matrix will be applied
+     */
+    public void transformEvent(float degrees, MotionEvent ev, boolean inverse) {
+        mTmpMatrix.setRotate(inverse ? -degrees : degrees);
+        ev.transform(mTmpMatrix);
+
+        // TODO: Add scaling back in based on degrees
+        /*
+        if (getWidth() > 0 && getHeight() > 0) {
+            float scale = ((float) getWidth()) / getHeight();
+            transform.postScale(scale, 1 / scale);
+        }
+        */
+    }
+
+    @SurfaceRotation
+    public static int getRotationForUserDegreesRotated(float degrees, int currentRotation) {
+        if (degrees == ORIENTATION_UNKNOWN) {
+            return currentRotation;
+        }
+
+        int threshold = 70;
+        switch (currentRotation) {
+            case ROTATION_0:
+                if (degrees > 180 && degrees < (360 - threshold)) {
+                    return ROTATION_90;
+                }
+                if (degrees < 180 && degrees > threshold) {
+                    return ROTATION_270;
+                }
+                break;
+            case ROTATION_270:
+                if (degrees < (90 - threshold) ||
+                        (degrees > (270 + threshold) && degrees < 360)) {
+                    return ROTATION_0;
+                }
+                if (degrees > (90 + threshold) && degrees < 180) {
+                    return ROTATION_180;
+                }
+                // flip from seascape to landscape
+                if (degrees > (180 + threshold) && degrees < 360) {
+                    return ROTATION_90;
+                }
+                break;
+            case ROTATION_180:
+                if (degrees < (180 - threshold)) {
+                    return ROTATION_270;
+                }
+                if (degrees > (180 + threshold)) {
+                    return ROTATION_90;
+                }
+                break;
+            case ROTATION_90:
+                if (degrees < (270 - threshold) && degrees > 90) {
+                    return ROTATION_180;
+                }
+                if (degrees > (270 + threshold) && degrees < 360
+                        || (degrees >= 0 && degrees < threshold)) {
+                    return ROTATION_0;
+                }
+                // flip from landscape to seascape
+                if (degrees > threshold && degrees < 180) {
+                    return ROTATION_270;
+                }
+                break;
+        }
+
+        return currentRotation;
+    }
+
+    public boolean isDisplayPhoneNatural() {
+        return mDisplayRotation == Surface.ROTATION_0 || mDisplayRotation == Surface.ROTATION_180;
+    }
+
+    /**
+     * Posts the transformation on the matrix representing the provided display rotation
+     */
+    public static void postDisplayRotation(@SurfaceRotation int displayRotation,
+            float screenWidth, float screenHeight, Matrix out) {
+        switch (displayRotation) {
+            case ROTATION_0:
+                return;
+            case ROTATION_90:
+                out.postRotate(270);
+                out.postTranslate(0, screenWidth);
+                break;
+            case ROTATION_180:
+                out.postRotate(180);
+                out.postTranslate(screenHeight, screenWidth);
+                break;
+            case ROTATION_270:
+                out.postRotate(90);
+                out.postTranslate(screenHeight, 0);
+                break;
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        boolean systemRotationOn = (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0;
+        return "["
+                + "this=" + extractObjectNameAndAddress(super.toString())
+                + " mOrientationHandler=" +
+                    extractObjectNameAndAddress(mOrientationHandler.toString())
+                + " mDisplayRotation=" + mDisplayRotation
+                + " mTouchRotation=" + mTouchRotation
+                + " mRecentsActivityRotation=" + mRecentsActivityRotation
+                + " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed()
+                + " mSystemRotation=" + systemRotationOn
+                + " mFlags=" + mFlags
+                + "]";
+    }
+
+    /**
+     * Returns the device profile based on expected launcher rotation
+     */
+    public DeviceProfile getLauncherDeviceProfile() {
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
+        // TODO also check the natural orientation is landscape or portrait
+        return  (mRecentsActivityRotation == ROTATION_90
+                || mRecentsActivityRotation == ROTATION_270)
+                ? idp.landscapeProfile
+                : idp.portraitProfile;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 4503a43..04308c8 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -21,50 +21,54 @@
 import android.os.Handler;
 
 import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.WrappedLauncherAnimationRunner;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.TransactionCompat;
 
-@FunctionalInterface
-public interface RemoteAnimationProvider {
+public abstract class RemoteAnimationProvider {
 
-    static final int Z_BOOST_BASE = 800570000;
+    LauncherAnimationRunner mAnimationRunner;
 
-    AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets);
+    public abstract AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets);
 
-    default ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
-        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
+    ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
+        mAnimationRunner = new LauncherAnimationRunner(handler,
                 false /* startAtFrontOfQueue */) {
 
             @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
-                    AnimationResult result) {
-                result.setAnimation(createWindowAnimation(targetCompats), context);
+            public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+                    RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+                result.setAnimation(createWindowAnimation(appTargets, wallpaperTargets), context);
             }
         };
+        final LauncherAnimationRunner wrapper = new WrappedLauncherAnimationRunner(
+                mAnimationRunner, false /* startAtFrontOfQueue */);
+
         return ActivityOptionsCompat.makeRemoteAnimation(
-                new RemoteAnimationAdapterCompat(runner, duration, 0));
+                new RemoteAnimationAdapterCompat(wrapper, duration, 0));
     }
 
     /**
-     * Prepares the given {@param targets} for a remote animation, and should be called with the
-     * transaction from the first frame of animation.
-     *
-     * @param boostModeTargets The mode indicating which targets to boost in z-order above other
-     *                         targets.
+     * @return the target with the lowest opaque layer for a certain app animation, or null.
      */
-    static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets,
-            TransactionCompat t, int boostModeTargets) {
-        for (RemoteAnimationTargetCompat target : targets) {
-            t.setLayer(target.leash, getLayer(target, boostModeTargets));
-            t.show(target.leash);
+    public static RemoteAnimationTargetCompat findLowestOpaqueLayerTarget(
+            RemoteAnimationTargetCompat[] appTargets, int mode) {
+        int lowestLayer = Integer.MAX_VALUE;
+        int lowestLayerIndex = -1;
+        for (int i = appTargets.length - 1; i >= 0; i--) {
+            RemoteAnimationTargetCompat target = appTargets[i];
+            if (target.mode == mode && !target.isTranslucent) {
+                int layer = target.prefixOrderIndex;
+                if (layer < lowestLayer) {
+                    lowestLayer = layer;
+                    lowestLayerIndex = i;
+                }
+            }
         }
-    }
-
-    static int getLayer(RemoteAnimationTargetCompat target, int boostModeTarget) {
-        return target.mode == boostModeTarget
-                ? Z_BOOST_BASE + target.prefixOrderIndex
-                : target.prefixOrderIndex;
+        return lowestLayerIndex != -1
+                ? appTargets[lowestLayerIndex]
+                : null;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java
deleted file mode 100644
index 1229293..0000000
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Queue;
-
-/**
- * Holds a collection of RemoteAnimationTargets, filtered by different properties.
- */
-public class RemoteAnimationTargetSet {
-
-    private final Queue<SyncRtSurfaceTransactionApplierCompat> mDependentTransactionAppliers =
-            new ArrayDeque<>(1);
-
-    public final RemoteAnimationTargetCompat[] unfilteredApps;
-    public final RemoteAnimationTargetCompat[] apps;
-    public final int targetMode;
-    public final boolean hasRecents;
-
-    public RemoteAnimationTargetSet(RemoteAnimationTargetCompat[] apps, int targetMode) {
-        ArrayList<RemoteAnimationTargetCompat> filteredApps = new ArrayList<>();
-        boolean hasRecents = false;
-        if (apps != null) {
-            for (RemoteAnimationTargetCompat target : apps) {
-                if (target.mode == targetMode) {
-                    filteredApps.add(target);
-                }
-
-                hasRecents |= target.activityType ==
-                        RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
-            }
-        }
-
-        this.unfilteredApps = apps;
-        this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]);
-        this.targetMode = targetMode;
-        this.hasRecents = hasRecents;
-    }
-
-    public RemoteAnimationTargetCompat findTask(int taskId) {
-        for (RemoteAnimationTargetCompat target : apps) {
-            if (target.taskId == taskId) {
-                return target;
-            }
-        }
-        return null;
-    }
-
-    public boolean isAnimatingHome() {
-        for (RemoteAnimationTargetCompat target : apps) {
-            if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public void addDependentTransactionApplier(SyncRtSurfaceTransactionApplierCompat delay) {
-        mDependentTransactionAppliers.add(delay);
-    }
-
-    public void release() {
-        SyncRtSurfaceTransactionApplierCompat applier = mDependentTransactionAppliers.poll();
-        if (applier == null) {
-            for (RemoteAnimationTargetCompat target : unfilteredApps) {
-                target.release();
-            }
-        } else {
-            applier.addAfterApplyCallback(this::release);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
index 40dd74b..958ee24 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
@@ -15,12 +15,12 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.quickstep.util.RemoteAnimationProvider.prepareTargetsForFirstFrame;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 
+import com.android.quickstep.RemoteAnimationTargets;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.TransactionCompat;
 
@@ -29,18 +29,21 @@
  */
 public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener {
 
-    private final RemoteAnimationTargetSet mTarget;
+    private final RemoteAnimationTargets mTarget;
     private boolean mFirstFrame = true;
 
-    public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] targets) {
-        mTarget = new RemoteAnimationTargetSet(targets, MODE_CLOSING);
+    public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets) {
+        mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_CLOSING);
     }
 
     @Override
     public void onAnimationUpdate(ValueAnimator valueAnimator) {
         TransactionCompat t = new TransactionCompat();
         if (mFirstFrame) {
-            prepareTargetsForFirstFrame(mTarget.unfilteredApps, t, MODE_CLOSING);
+            for (RemoteAnimationTargetCompat target : mTarget.unfilteredApps) {
+                t.show(target.leash);
+            }
             mFirstFrame = false;
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
new file mode 100644
index 0000000..a770e8e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.WindowInsets.Type;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.WindowBounds;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to hold the information abound a window bounds for split screen
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class SplitScreenBounds {
+
+    public static final SplitScreenBounds INSTANCE = new SplitScreenBounds();
+    private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
+
+    @Nullable
+    private WindowBounds mBounds;
+
+    private SplitScreenBounds() { }
+
+    @UiThread
+    public void setSecondaryWindowBounds(@NonNull WindowBounds bounds) {
+        if (!bounds.equals(mBounds)) {
+            mBounds = bounds;
+            for (OnChangeListener listener : mListeners) {
+                listener.onSecondaryWindowBoundsChanged();
+            }
+        }
+    }
+
+    public @NonNull WindowBounds getSecondaryWindowBounds(Context context) {
+        if (mBounds == null) {
+            mBounds = createDefaultWindowBounds(context);
+        }
+        return mBounds;
+    }
+
+    /**
+     * Creates window bounds as 50% of device size
+     */
+    private static WindowBounds createDefaultWindowBounds(Context context) {
+        WindowMetrics wm = context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
+        Insets insets = wm.getWindowInsets().getInsets(Type.systemBars());
+
+        WindowBounds bounds = new WindowBounds(wm.getBounds(),
+                new Rect(insets.left, insets.top, insets.right, insets.bottom));
+        int rotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
+        int halfDividerSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
+
+        if (rotation == ROTATION_0 || rotation == ROTATION_180) {
+            bounds.bounds.top = bounds.insets.top + bounds.availableSize.y / 2 + halfDividerSize;
+            bounds.insets.top = 0;
+        } else {
+            bounds.bounds.left = bounds.insets.left + bounds.availableSize.x / 2 + halfDividerSize;
+            bounds.insets.left = 0;
+        }
+        return new WindowBounds(bounds.bounds, bounds.insets);
+    }
+
+    public void addOnChangeListener(OnChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeOnChangeListener(OnChangeListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Interface to receive window bounds changes
+     */
+    public interface OnChangeListener {
+
+        /**
+         * Called when window bounds for secondary window changes
+         */
+        void onSecondaryWindowBoundsChanged();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
new file mode 100644
index 0000000..d87feec
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.util.Log;
+
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+
+import java.util.LinkedHashMap;
+import java.util.function.Predicate;
+
+/**
+ * A simple LRU cache for task key entries
+ * @param <V> The type of the value
+ */
+public class TaskKeyLruCache<V> {
+
+    private final MyLinkedHashMap<V> mMap;
+
+    public TaskKeyLruCache(int maxSize) {
+        mMap = new MyLinkedHashMap<>(maxSize);
+    }
+
+    /**
+     * Removes all entries from the cache
+     */
+    public synchronized void evictAll() {
+        mMap.clear();
+    }
+
+    /**
+     * Removes a particular entry from the cache
+     */
+    public synchronized void remove(TaskKey key) {
+        mMap.remove(key.id);
+    }
+
+    /**
+     * Removes all entries matching keyCheck
+     */
+    public synchronized void removeAll(Predicate<TaskKey> keyCheck) {
+        mMap.entrySet().removeIf(e -> keyCheck.test(e.getValue().mKey));
+    }
+
+    /**
+     * Gets the entry if it is still valid
+     */
+    public synchronized V getAndInvalidateIfModified(TaskKey key) {
+        Entry<V> entry = mMap.get(key.id);
+
+        if (entry != null && entry.mKey.windowingMode == key.windowingMode
+                && entry.mKey.lastActiveTime == key.lastActiveTime) {
+            return entry.mValue;
+        } else {
+            remove(key);
+            return null;
+        }
+    }
+
+    /**
+     * Adds an entry to the cache, optionally evicting the last accessed entry
+     */
+    public final synchronized void put(TaskKey key, V value) {
+        if (key != null && value != null) {
+            mMap.put(key.id, new Entry<>(key, value));
+        } else {
+            Log.e("TaskKeyCache", "Unexpected null key or value: " + key + ", " + value);
+        }
+    }
+
+    /**
+     * Updates the cache entry if it is already present in the cache
+     */
+    public synchronized void updateIfAlreadyInCache(int taskId, V data) {
+        Entry<V> entry = mMap.get(taskId);
+        if (entry != null) {
+            entry.mValue = data;
+        }
+    }
+
+    private static class Entry<V> {
+
+        final TaskKey mKey;
+        V mValue;
+
+        Entry(TaskKey key, V value) {
+            mKey = key;
+            mValue = value;
+        }
+
+        @Override
+        public int hashCode() {
+            return mKey.id;
+        }
+    }
+
+    private static class MyLinkedHashMap<V> extends LinkedHashMap<Integer, Entry<V>> {
+
+        private final int mMaxSize;
+
+        MyLinkedHashMap(int maxSize) {
+            super(0, 0.75f, true /* accessOrder */);
+            mMaxSize = maxSize;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Entry<Integer, TaskKeyLruCache.Entry<V>> eldest) {
+            return size() > mMaxSize;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 3320dae..19e278b 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -35,19 +36,21 @@
 import android.util.AttributeSet;
 import android.view.animation.Interpolator;
 
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.uioverrides.states.OverviewState;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.ShelfPeekAnim;
 
 /**
  * Scrim used for all-apps and shelf in Overview
@@ -56,7 +59,8 @@
  *    From normal state to overview state, the shelf just fades in and does not move
  *    From overview state to all-apps state the shelf moves up and fades in to cover the screen
  */
-public class ShelfScrimView extends ScrimView implements NavigationModeChangeListener {
+public class ShelfScrimView extends ScrimView<BaseQuickstepLauncher>
+        implements NavigationModeChangeListener {
 
     // If the progress is more than this, shelf follows the finger, otherwise it moves faster to
     // cover the whole screen
@@ -73,6 +77,7 @@
     private final float mRadius;
     private final int mMaxScrimAlpha;
     private final Paint mPaint;
+    private final OnboardingPrefs mOnboardingPrefs;
 
     // Mid point where the alpha changes
     private int mMidAlpha;
@@ -86,7 +91,6 @@
 
     private float mShiftRange;
 
-    private final float mShelfOffset;
     private float mTopOffset;
     private float mShelfTop;
     private float mShelfTopAtThreshold;
@@ -99,6 +103,7 @@
     private boolean mRemainingScreenPathValid = false;
 
     private Mode mSysUINavigationMode;
+    private boolean mIsTwoZoneSwipeModel;
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -107,8 +112,8 @@
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mOnboardingPrefs = mLauncher.getOnboardingPrefs();
 
-        mShelfOffset = context.getResources().getDimension(R.dimen.shelf_surface_offset);
         // Just assume the easiest UI for now, until we have the proper layout information.
         mDrawingFlatColor = true;
     }
@@ -140,9 +145,11 @@
             // Show the shelf more quickly before reaching overview progress.
             mBeforeMidProgressColorInterpolator = ACCEL_2;
             mAfterMidProgressColorInterpolator = ACCEL;
+            mIsTwoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get();
         } else {
             mBeforeMidProgressColorInterpolator = ACCEL;
             mAfterMidProgressColorInterpolator = Interpolators.clampToProgress(ACCEL, 0.5f, 1f);
+            mIsTwoZoneSwipeModel = false;
         }
     }
 
@@ -155,12 +162,19 @@
             mRemainingScreenPathValid = false;
             mShiftRange = mLauncher.getAllAppsController().getShiftRange();
 
+            Context context = getContext();
             if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
-                mMidProgress = 1;
                 mDragHandleProgress = 1;
-                mMidAlpha = 0;
+                if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
+                        && SysUINavigationMode.removeShelfFromOverview(context)) {
+                    // Fade in all apps background quickly to distinguish from swiping from nav bar.
+                    mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
+                    mMidProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
+                } else {
+                    mMidAlpha = 0;
+                    mMidProgress = 1;
+                }
             } else {
-                Context context = getContext();
                 mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
                 mMidProgress =  OVERVIEW.getVerticalProgress(mLauncher);
                 Rect hotseatPadding = dp.getHotseatLayoutPadding();
@@ -170,10 +184,11 @@
                         Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
                 mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
-            mTopOffset = dp.getInsets().top - mShelfOffset;
+            mTopOffset = dp.getInsets().top - mDragHandleSize.y;
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
         }
         updateColors();
+        updateSysUiColors();
         updateDragHandleAlpha();
         invalidate();
     }
@@ -181,12 +196,15 @@
     @Override
     public void updateColors() {
         super.updateColors();
+        mDragHandleOffset = 0;
         if (mDrawingFlatColor) {
-            mDragHandleOffset = 0;
             return;
         }
 
-        mDragHandleOffset = mShelfOffset - mDragHandleSize;
+        if (mProgress < mDragHandleProgress) {
+            mDragHandleOffset = mShiftRange * (mDragHandleProgress - mProgress);
+        }
+
         if (mProgress >= SCRIM_CATCHUP_THRESHOLD) {
             mShelfTop = mShiftRange * mProgress + mTopOffset;
         } else {
@@ -197,12 +215,10 @@
         if (mProgress >= 1) {
             mRemainingScreenColor = 0;
             mShelfColor = 0;
-            ShelfPeekAnim shelfPeekAnim = ((QuickstepAppTransitionManagerImpl)
-                    mLauncher.getAppTransitionManager()).getShelfPeekAnim();
             LauncherState state = mLauncher.getStateManager().getState();
             if (mSysUINavigationMode == Mode.NO_BUTTON
                     && (state == BACKGROUND_APP || state == QUICK_SWITCH)
-                    && shelfPeekAnim.isPeeking()) {
+                    && mLauncher.getShelfPeekAnim().isPeeking()) {
                 // Show the shelf background when peeking during swipe up.
                 mShelfColor = setColorAlphaBound(mEndScrim, mMidAlpha);
             }
@@ -224,13 +240,32 @@
                             (float) 0, LINEAR));
             mRemainingScreenColor = setColorAlphaBound(mScrimColor, remainingScrimAlpha);
         }
+    }
 
-        if (mProgress < mDragHandleProgress) {
-            mDragHandleOffset += mShiftRange * (mDragHandleProgress - mProgress);
+    @Override
+    protected void updateSysUiColors() {
+        if (mDrawingFlatColor) {
+            super.updateSysUiColors();
+        } else {
+            // Use a light system UI (dark icons) if all apps is behind at least half of the
+            // status bar.
+            boolean forceChange = mShelfTop <= mLauncher.getDeviceProfile().getInsets().top / 2f;
+            if (forceChange) {
+                mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
+            } else {
+                mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
+            }
         }
     }
 
     @Override
+    protected boolean shouldDragHandleBeVisible() {
+        boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
+                && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
+        return needsAllAppsEdu || super.shouldDragHandleBeVisible();
+    }
+
+    @Override
     protected void onDraw(Canvas canvas) {
         drawBackground(canvas);
         drawDragHandle(canvas);
@@ -276,4 +311,9 @@
         mPaint.setColor(mShelfColor);
         canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint);
     }
+
+    @Override
+    public float getVisualTop() {
+        return mShelfTop;
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
index 7801775..5904fcd 100644
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
@@ -24,6 +24,7 @@
 import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.os.Process;
 import android.view.View;
 
@@ -35,7 +36,6 @@
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.model.AppLaunchTracker;
 
 import org.junit.After;
@@ -60,7 +60,7 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(mTargetContext)
+        List<LauncherActivityInfo> activities = mTargetContext.getSystemService(LauncherApps.class)
                 .getActivityList(null, Process.myUserHandle());
         mSampleApp1 = activities.get(0);
         mSampleApp2 = activities.get(1);
@@ -88,7 +88,6 @@
      */
     @Test
     public void testPredictionExistsInAllApps() {
-        mDevice.pressHome();
         mLauncher.pressHome().switchToAllApps();
 
         // Dispatch an update
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index a7c33a9..ccfa3fc 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -11,6 +11,7 @@
 import android.app.PendingIntent;
 import android.app.usage.UsageStatsManager;
 import android.content.Intent;
+import android.os.Build;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -33,6 +34,9 @@
 
     @Test
     public void testToast() throws Exception {
+        // b/150303529
+        if (Build.MODEL.contains("Cuttlefish")) return;
+
         startAppFast(CALCULATOR_PACKAGE);
 
         final UsageStatsManager usageStatsManager =
@@ -68,7 +72,7 @@
 
     private DigitalWellBeingToast getToast() {
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(OVERVIEW));
-        waitForState("Launcher internal state didn't switch to Overview", OVERVIEW);
+        waitForState("Launcher internal state didn't switch to Overview", () -> OVERVIEW);
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 8c11c1c..b9e0f62 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -22,11 +22,18 @@
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
 import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
+import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT;
+import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp;
+import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast;
+import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity;
+import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification;
 import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand;
-import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
@@ -41,11 +48,16 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
+import com.android.launcher3.Utilities;
+import com.android.launcher3.tapl.BaseOverview;
 import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.FailureRewriterRule;
 import com.android.launcher3.util.rule.FailureWatcher;
-import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
+import com.android.quickstep.views.RecentsView;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -54,11 +66,11 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
+import java.util.function.Consumer;
+import java.util.function.Function;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-/**
- * TODO: Fix fallback when quickstep is enabled
- */
 public class FallbackRecentsTest {
 
     private final UiDevice mDevice;
@@ -80,10 +92,17 @@
         mDevice = UiDevice.getInstance(instrumentation);
         mDevice.setOrientationNatural();
         mLauncher = new LauncherInstrumentation();
+        // b/143488140
+        //mLauncher.enableCheckEventsForSuccessfulGestures();
 
-        mOrderSensitiveRules = RuleChain.
-                outerRule(new NavigationModeSwitchRule(mLauncher)).
-                around(new FailureWatcher(mDevice));
+        if (TestHelpers.isInLauncherProcess()) {
+            Utilities.enableRunningInTestHarnessForTests();
+        }
+
+        mOrderSensitiveRules = RuleChain
+                .outerRule(new FailureRewriterRule())
+                .around(new NavigationModeSwitchRule(mLauncher))
+                .around(new FailureWatcher(mDevice));
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
                 getHomeIntentInPackage(context),
@@ -101,6 +120,9 @@
                     TestCommandReceiver.callCommand(TestCommandReceiver.DISABLE_TEST_LAUNCHER);
                     UiDevice.getInstance(getInstrumentation()).executeShellCommand(
                             getLauncherCommand(getLauncherInMyProcess()));
+                    // b/143488140
+                    mDevice.pressHome();
+                    mDevice.waitForIdle();
                 }
             }
         };
@@ -109,9 +131,14 @@
                     TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
                     getString("result"));
         }
+        // b/143488140
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
     }
 
-    @NavigationModeSwitch(mode = THREE_BUTTON)
+    // b/143488140
+    //@NavigationModeSwitch
     @Test
     public void goToOverviewFromHome() {
         mDevice.pressHome();
@@ -121,7 +148,8 @@
         mLauncher.getBackground().switchToOverview();
     }
 
-    @NavigationModeSwitch(mode = THREE_BUTTON)
+    // b/143488140
+    //@NavigationModeSwitch
     @Test
     public void goToOverviewFromApp() {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
@@ -129,15 +157,88 @@
         mLauncher.getBackground().switchToOverview();
     }
 
-    private void startAppFast(String packageName) {
-        final Instrumentation instrumentation = getInstrumentation();
-        final Intent intent = instrumentation.getContext().getPackageManager().
-                getLaunchIntentForPackage(packageName);
-        intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        instrumentation.getTargetContext().startActivity(intent);
-        assertTrue(packageName + " didn't start",
-                mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), WAIT_TIME_MS));
+    protected void executeOnRecents(Consumer<RecentsActivity> f) {
+        getFromRecents(r -> {
+            f.accept(r);
+            return true;
+        });
     }
 
+    protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
+        if (!TestHelpers.isInLauncherProcess()) return null;
+        Object[] result = new Object[1];
+        Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
+            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+            if (activity == null) {
+                return false;
+            }
+            result[0] = f.apply(activity);
+            return true;
+        }).get(), DEFAULT_UI_TIMEOUT, mLauncher);
+        return (T) result[0];
+    }
+
+    private BaseOverview pressHomeAndGoToOverview() {
+        mDevice.pressHome();
+        return mLauncher.getBackground().switchToOverview();
+    }
+
+    // b/143488140
+    //@NavigationModeSwitch
+    @Test
+    public void testOverview() {
+        startAppFast(getAppPackageName());
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startTestActivity(2);
+        Wait.atMost("Expected three apps in the task list",
+                () -> 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));
+
+        // Test flinging forward and backward.
+        overview.flingForward();
+        final Integer currentTaskAfterFlingForward = getFromRecents(this::getCurrentOverviewPage);
+        executeOnRecents(recents -> assertTrue("Current task in Overview is still 0",
+                currentTaskAfterFlingForward > 0));
+
+        overview.flingBackward();
+        executeOnRecents(recents -> assertTrue("Flinging back in Overview did nothing",
+                getCurrentOverviewPage(recents) < currentTaskAfterFlingForward));
+
+        // Test opening a task.
+        overview = pressHomeAndGoToOverview();
+
+        OverviewTask task = overview.getCurrentTask();
+        assertNotNull("overview.getCurrentTask() returned null (1)", task);
+        assertNotNull("OverviewTask.open returned null", task.open());
+        assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject(
+                By.pkg(getAppPackageName()).text("TestActivity2")),
+                DEFAULT_UI_TIMEOUT));
+
+
+        // Test dismissing a task.
+        overview = pressHomeAndGoToOverview();
+        final Integer numTasks = getFromRecents(this::getTaskCount);
+        task = overview.getCurrentTask();
+        assertNotNull("overview.getCurrentTask() returned null (2)", task);
+        task.dismiss();
+        executeOnRecents(
+                recents -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
+                        numTasks - 1, getTaskCount(recents)));
+
+        // Test dismissing all tasks.
+        pressHomeAndGoToOverview().dismissAllTasks();
+        assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg(
+                mOtherLauncherActivity.packageName)), WAIT_TIME_MS));
+    }
+
+    private int getCurrentOverviewPage(RecentsActivity recents) {
+        return recents.<RecentsView>getOverviewPanel().getCurrentPage();
+    }
+
+    private int getTaskCount(RecentsActivity recents) {
+        return recents.<RecentsView>getOverviewPanel().getTaskViewCount();
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index c2197ab..40265c4 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -30,10 +30,13 @@
 import android.content.pm.PackageManager;
 import android.util.Log;
 
+import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.systemui.shared.system.QuickStepContract;
 
@@ -46,7 +49,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Test rule that allows executing a test with Quickstep on and then Quickstep off.
@@ -56,6 +58,8 @@
 
     static final String TAG = "QuickStepOnOffRule";
 
+    public static final int WAIT_TIME_MS = 10000;
+
     public enum Mode {
         THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL
     }
@@ -69,6 +73,9 @@
 
     private final LauncherInstrumentation mLauncher;
 
+    static final SysUINavigationMode SYS_UI_NAVIGATION_MODE =
+            SysUINavigationMode.INSTANCE.get(getInstrumentation().getTargetContext());
+
     public NavigationModeSwitchRule(LauncherInstrumentation launcher) {
         mLauncher = launcher;
     }
@@ -79,26 +86,13 @@
                 description.getAnnotation(NavigationModeSwitch.class) != null) {
             Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode();
             return new Statement() {
-                private void assertTrue(String message, boolean condition) {
-                    if(!condition) {
-                        final AssertionError assertionError = new AssertionError(message);
-                        FailureWatcher.onError(mLauncher.getDevice(), description, assertionError);
-                        throw assertionError;
-                    }
-                }
-
                 @Override
                 public void evaluate() throws Throwable {
                     mLauncher.enableDebugTracing();
                     final Context context = getInstrumentation().getContext();
                     final int currentInteractionMode =
                             LauncherInstrumentation.getCurrentInteractionMode(context);
-                    final String prevOverlayPkg =
-                            QuickStepContract.isGesturalMode(currentInteractionMode)
-                                    ? NAV_BAR_MODE_GESTURAL_OVERLAY
-                                    : QuickStepContract.isSwipeUpMode(currentInteractionMode)
-                                            ? NAV_BAR_MODE_2BUTTON_OVERLAY
-                                            : NAV_BAR_MODE_3BUTTON_OVERLAY;
+                    final String prevOverlayPkg = getCurrentOverlayPackage(currentInteractionMode);
                     final LauncherInstrumentation.NavigationModel originalMode =
                             mLauncher.getNavigationModel();
                     try {
@@ -111,114 +105,51 @@
                         if (mode == THREE_BUTTON || mode == ALL) {
                             evaluateWithThreeButtons();
                         }
-                    } catch (Exception e) {
-                        Log.e(TAG, "Exception", e);
+                    } catch (Throwable e) {
+                        Log.e(TAG, "Error", e);
                         throw e;
                     } finally {
-                        assertTrue("Couldn't set overlay",
-                                setActiveOverlay(prevOverlayPkg, originalMode));
+                        Log.d(TAG, "In Finally block");
+                        assertTrue(mLauncher, "Couldn't set overlay",
+                                setActiveOverlay(mLauncher, prevOverlayPkg, originalMode,
+                                        description), description);
                     }
                 }
 
                 private void evaluateWithThreeButtons() throws Throwable {
-                    if (setActiveOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.THREE_BUTTON)) {
+                    if (setActiveOverlay(mLauncher, NAV_BAR_MODE_3BUTTON_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.THREE_BUTTON, description)) {
                         base.evaluate();
                     }
                 }
 
                 private void evaluateWithTwoButtons() throws Throwable {
-                    if (setActiveOverlay(NAV_BAR_MODE_2BUTTON_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.TWO_BUTTON)) {
+                    if (setActiveOverlay(mLauncher, NAV_BAR_MODE_2BUTTON_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.TWO_BUTTON, description)) {
                         base.evaluate();
                     }
                 }
 
                 private void evaluateWithZeroButtons() throws Throwable {
-                    if (setActiveOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.ZERO_BUTTON)) {
+                    if (setActiveOverlay(mLauncher, NAV_BAR_MODE_GESTURAL_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.ZERO_BUTTON, description)) {
                         base.evaluate();
                     }
                 }
-
-                private boolean packageExists(String packageName) {
-                    try {
-                        PackageManager pm = getInstrumentation().getContext().getPackageManager();
-                        if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) {
-                            return false;
-                        }
-                    } catch (PackageManager.NameNotFoundException e) {
-                        return false;
-                    }
-                    return true;
-                }
-
-                private boolean setActiveOverlay(String overlayPackage,
-                        LauncherInstrumentation.NavigationModel expectedMode) throws Exception {
-                    if (!packageExists(overlayPackage)) {
-                        Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
-                        return false;
-                    }
-
-                    setOverlayPackageEnabled(NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            overlayPackage == NAV_BAR_MODE_3BUTTON_OVERLAY);
-                    setOverlayPackageEnabled(NAV_BAR_MODE_2BUTTON_OVERLAY,
-                            overlayPackage == NAV_BAR_MODE_2BUTTON_OVERLAY);
-                    setOverlayPackageEnabled(NAV_BAR_MODE_GESTURAL_OVERLAY,
-                            overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY);
-
-                    if (currentSysUiNavigationMode() != expectedMode) {
-                        final CountDownLatch latch = new CountDownLatch(1);
-                        final Context targetContext = getInstrumentation().getTargetContext();
-                        final SysUINavigationMode.NavigationModeChangeListener listener =
-                                newMode -> {
-                                    if (LauncherInstrumentation.getNavigationModel(newMode.resValue)
-                                            == expectedMode) {
-                                        latch.countDown();
-                                    }
-                                };
-                        final SysUINavigationMode sysUINavigationMode =
-                                SysUINavigationMode.INSTANCE.get(targetContext);
-                        targetContext.getMainExecutor().execute(() ->
-                                sysUINavigationMode.addModeChangeListener(listener));
-                        latch.await(60, TimeUnit.SECONDS);
-                        targetContext.getMainExecutor().execute(() ->
-                                sysUINavigationMode.removeModeChangeListener(listener));
-                        assertTrue("Navigation mode didn't change to " + expectedMode,
-                                currentSysUiNavigationMode() == expectedMode);
-                    }
-
-                    for (int i = 0; i != 100; ++i) {
-                        if (mLauncher.getNavigationModel() == expectedMode) break;
-                        Thread.sleep(100);
-                    }
-                    assertTrue("Couldn't switch to " + overlayPackage,
-                            mLauncher.getNavigationModel() == expectedMode);
-
-                    for (int i = 0; i != 100; ++i) {
-                        if (mLauncher.getNavigationModeMismatchError() == null) break;
-                        Thread.sleep(100);
-                    }
-                    final String error = mLauncher.getNavigationModeMismatchError();
-                    assertTrue("Switching nav mode: " + error, error == null);
-
-                    Thread.sleep(5000);
-                    return true;
-                }
-
-                private void setOverlayPackageEnabled(String overlayPackage, boolean enable)
-                        throws Exception {
-                    Log.d(TAG, "setOverlayPackageEnabled: " + overlayPackage + " " + enable);
-                    final String action = enable ? "enable" : "disable";
-                    UiDevice.getInstance(getInstrumentation()).executeShellCommand(
-                            "cmd overlay " + action + " " + overlayPackage);
-                }
             };
         } else {
             return base;
         }
     }
 
+    public static String getCurrentOverlayPackage(int currentInteractionMode) {
+        return QuickStepContract.isGesturalMode(currentInteractionMode)
+                ? NAV_BAR_MODE_GESTURAL_OVERLAY
+                : QuickStepContract.isSwipeUpMode(currentInteractionMode)
+                        ? NAV_BAR_MODE_2BUTTON_OVERLAY
+                        : NAV_BAR_MODE_3BUTTON_OVERLAY;
+    }
+
     private static LauncherInstrumentation.NavigationModel currentSysUiNavigationMode() {
         return LauncherInstrumentation.getNavigationModel(
                 SysUINavigationMode.getMode(
@@ -226,4 +157,82 @@
                                 getTargetContext()).
                         resValue);
     }
+
+    public static boolean setActiveOverlay(LauncherInstrumentation launcher, String overlayPackage,
+            LauncherInstrumentation.NavigationModel expectedMode, Description description)
+            throws Exception {
+        if (!packageExists(overlayPackage)) {
+            Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
+            return false;
+        }
+
+        Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
+        UiDevice.getInstance(getInstrumentation()).executeShellCommand(
+                "cmd overlay enable-exclusive " + overlayPackage);
+
+        if (currentSysUiNavigationMode() != expectedMode) {
+            final CountDownLatch latch = new CountDownLatch(1);
+            final Context targetContext = getInstrumentation().getTargetContext();
+            final SysUINavigationMode.NavigationModeChangeListener listener =
+                    newMode -> {
+                        if (LauncherInstrumentation.getNavigationModel(newMode.resValue)
+                                == expectedMode) {
+                            latch.countDown();
+                        }
+                    };
+            targetContext.getMainExecutor().execute(() ->
+                    SYS_UI_NAVIGATION_MODE.addModeChangeListener(listener));
+            // b/139137636
+//            latch.await(60, TimeUnit.SECONDS);
+            targetContext.getMainExecutor().execute(() ->
+                    SYS_UI_NAVIGATION_MODE.removeModeChangeListener(listener));
+
+            Wait.atMost(() -> "Navigation mode didn't change to " + expectedMode,
+                    () -> currentSysUiNavigationMode() == expectedMode, 60000 /* b/148422894 */,
+                    launcher);
+            // b/139137636
+//            assertTrue(launcher, "Navigation mode didn't change to " + expectedMode,
+//                    currentSysUiNavigationMode() == expectedMode, description);
+
+        }
+
+        Wait.atMost("Couldn't switch to " + overlayPackage,
+                () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
+
+        Wait.atMost(() -> "Switching nav mode: "
+                        + launcher.getNavigationModeMismatchError(),
+                () -> launcher.getNavigationModeMismatchError() == null,
+                60000 /* b/148422894 */, launcher);
+        AbstractLauncherUiTest.checkDetectedLeaks(launcher);
+        return true;
+    }
+
+    private static boolean packageExists(String packageName) {
+        try {
+            PackageManager pm = getInstrumentation().getContext().getPackageManager();
+            if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) {
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private static void assertTrue(LauncherInstrumentation launcher, String message,
+            boolean condition, Description description) {
+        if (launcher.getDevice().hasObject(By.textStartsWith(""))) {
+            // The condition above is "screen is not empty". We are not treating
+            // "Screen is empty" as an anomaly here. It's an acceptable state when
+            // Launcher just starts under instrumentation.
+            launcher.checkForAnomaly();
+        }
+        if (!condition) {
+            final AssertionError assertionError = new AssertionError(message);
+            if (description != null) {
+                FailureWatcher.onError(launcher.getDevice(), description, assertionError);
+            }
+            throw assertionError;
+        }
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 34eb7f8..79ddf7a 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -58,9 +58,9 @@
     }
 
     @Test
-    public void onTaskRemoved_reloadsAllTasks() {
+    public void onTaskRemoved_doesNotFetchTasks() {
         mRecentTasksList.onTaskRemoved(0);
-        verify(mockActivityManagerWrapper, times(1))
+        verify(mockActivityManagerWrapper, times(0))
                 .getRecentTasks(anyInt(), anyInt());
     }
 
@@ -77,7 +77,7 @@
         when(mockActivityManagerWrapper.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(Collections.singletonList(recentTaskInfo));
 
-        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, true);
+        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, true);
 
         assertEquals(1, taskList.size());
         assertNull(taskList.get(0).taskDescription.getLabel());
@@ -91,7 +91,7 @@
         when(mockActivityManagerWrapper.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(Collections.singletonList(recentTaskInfo));
 
-        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, false);
+        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, false);
 
         assertEquals(1, taskList.size());
         assertEquals(taskDescription, taskList.get(0).taskDescription.getLabel());
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 2111e2c..9732cdc 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -16,8 +16,8 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.util.RaceConditionTracker.enterEvt;
-import static com.android.launcher3.util.RaceConditionTracker.exitEvt;
+import static com.android.launcher3.util.RaceConditionReproducer.enterEvt;
+import static com.android.launcher3.util.RaceConditionReproducer.exitEvt;
 
 import android.content.Intent;
 
@@ -45,6 +45,8 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
+        // b/143488140
+        mLauncher.pressHome();
         // Start an activity where the gestures start.
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
     }
@@ -98,5 +100,6 @@
             // The test action.
             mLauncher.getBackground().switchToOverview();
         }
+        mLauncher.pressHome();
     }
-}
\ No newline at end of file
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 8cd3bb6..bf093fd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Intent;
-import android.os.RemoteException;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -43,8 +42,8 @@
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.views.RecentsView;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -55,6 +54,18 @@
     public void setUp() throws Exception {
         super.setUp();
         TaplTestsLauncher3.initialize(this);
+        executeOnLauncher(launcher -> {
+            RecentsView recentsView = launcher.getOverviewPanel();
+            recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true);
+        });
+    }
+
+    @After
+    public void tearDown() {
+        executeOnLauncher(launcher -> {
+            RecentsView recentsView = launcher.getOverviewPanel();
+            recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false);
+        });
     }
 
     private void startTestApps() throws Exception {
@@ -69,26 +80,21 @@
     }
 
     @Test
-    @PortraitLandscape
-    @Ignore // Enable after b/131115533
-    public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
-        mDevice.pressRecentApps();
-        waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
-
-        assertNotNull("getOverview() returned null", mLauncher.getOverview());
-    }
-
-    @Test
     @NavigationModeSwitch
     @PortraitLandscape
     public void testWorkspaceSwitchToAllApps() {
         assertNotNull("switchToAllApps() returned null",
                 mLauncher.getWorkspace().switchToAllApps());
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
     }
 
     @Test
     public void testAllAppsFromOverview() throws Exception {
+        if (!mLauncher.hasAllAppsInOverview()) {
+            return;
+        }
+
         // Test opening all apps from Overview.
         assertNotNull("switchToAllApps() returned null",
                 mLauncher.getWorkspace().switchToOverview().switchToAllApps());
@@ -100,9 +106,10 @@
     @PortraitLandscape
     public void testOverview() throws Exception {
         startTestApps();
+        // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
         executeOnLauncher(
                 launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3));
 
@@ -111,14 +118,16 @@
                 0, getCurrentOverviewPage(launcher)));
 
         overview.flingForward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
         final Integer currentTaskAfterFlingForward = getFromLauncher(
                 launcher -> getCurrentOverviewPage(launcher));
         executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0",
                 currentTaskAfterFlingForward > 0));
 
         overview.flingBackward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
         executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing",
                 getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward));
 
@@ -137,7 +146,7 @@
         // Test dismissing a task.
         overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
         final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher));
         task = overview.getCurrentTask();
         assertNotNull("overview.getCurrentTask() returned null (2)", task);
@@ -146,35 +155,35 @@
                 launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
                         numTasks - 1, getTaskCount(launcher)));
 
-        if (!TestHelpers.isInLauncherProcess() ||
-                getFromLauncher(launcher -> !launcher.getDeviceProfile().isLandscape)) {
+        if (mLauncher.hasAllAppsInOverview() && (!TestHelpers.isInLauncherProcess()
+                || getFromLauncher(launcher -> !launcher.getDeviceProfile().isLandscape))) {
             // Test switching to all apps and back.
             final AllAppsFromOverview allApps = overview.switchToAllApps();
             assertNotNull("overview.switchToAllApps() returned null (1)", allApps);
             assertTrue("Launcher internal state is not All Apps (1)",
-                    isInState(LauncherState.ALL_APPS));
+                    isInState(() -> LauncherState.ALL_APPS));
 
             overview = allApps.switchBackToOverview();
             assertNotNull("allApps.switchBackToOverview() returned null", overview);
             assertTrue("Launcher internal state didn't switch to Overview",
-                    isInState(LauncherState.OVERVIEW));
+                    isInState(() -> LauncherState.OVERVIEW));
 
             // Test UIDevice.pressBack()
             overview.switchToAllApps();
             assertNotNull("overview.switchToAllApps() returned null (2)", allApps);
             assertTrue("Launcher internal state is not All Apps (2)",
-                    isInState(LauncherState.ALL_APPS));
+                    isInState(() -> LauncherState.ALL_APPS));
             mDevice.pressBack();
             mLauncher.getOverview();
         }
 
         // Test UIDevice.pressHome, once we are in AllApps.
         mDevice.pressHome();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
 
         // Test dismissing all tasks.
         mLauncher.getWorkspace().switchToOverview().dismissAllTasks();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
         executeOnLauncher(
                 launcher -> assertEquals("Still have tasks after dismissing all",
                         0, getTaskCount(launcher)));
@@ -190,9 +199,14 @@
 
     @Test
     public void testAppIconLaunchFromAllAppsFromOverview() throws Exception {
+        if (!mLauncher.hasAllAppsInOverview()) {
+            return;
+        }
+
         final AllApps allApps =
                 mLauncher.getWorkspace().switchToOverview().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
 
         TaplTestsLauncher3.runIconLaunchFromAllAppsTest(this, allApps);
     }
@@ -204,7 +218,7 @@
         assertNotNull("Workspace.switchToOverview() returned null",
                 mLauncher.pressHome().switchToOverview());
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
     }
 
     @Test
@@ -216,7 +230,7 @@
 
         assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
     }
 
     private Background getAndAssertBackground() {
@@ -239,9 +253,11 @@
         TaplTestsLauncher3.runAllAppsTest(this, mLauncher.getAllApps());
 
         // Testing pressHome.
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
         assertNotNull("pressHome returned null", mLauncher.pressHome());
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home",
+                isInState(() -> LauncherState.NORMAL));
         assertNotNull("getHome returned null", mLauncher.getWorkspace());
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
new file mode 100644
index 0000000..115294a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static com.android.launcher3.common.WidgetUtils.createWidgetInfo;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER;
+import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.RemoteViews;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.Suppress;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+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;
+import com.android.launcher3.testcomponent.ListViewService;
+import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory;
+import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.ui.TestViewHelpers;
+import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.util.function.IntConsumer;
+
+/**
+ * Test to verify view inflation does not happen during swipe up.
+ * To verify view inflation, we setup a dummy ViewConfiguration and check if any call to that class
+ * does from a View.init method or not.
+ *
+ * Alternative approaches considered:
+ *    Overriding LayoutInflater: This does not cover views initialized
+ *        directly (ex: new LinearLayout)
+ *    Using ExtendedMockito: Mocking static methods from platform classes (loaded in zygote) makes
+ *        the main thread extremely slow and untestable
+ *
+ * Suppressed until b/141579810 is resolved
+ */
+@Suppress
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest {
+
+    private ContentResolver mResolver;
+    private SparseArray<ViewConfiguration> mConfigMap;
+    private InitTracker mInitTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // Workaround for b/142351228, when there are no activities, the system may not destroy the
+        // activity correctly for activities under instrumentation, which can leave two concurrent
+        // activities, which changes the order in which the activities are cleaned up (overlapping
+        // stop and start) leading to all sort of issues. To workaround this, ensure that the test
+        // is started only after starting another app.
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+
+        TaplTestsLauncher3.initialize(this);
+
+        mResolver = mTargetContext.getContentResolver();
+        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+
+        // Get static configuration map
+        Field field = ViewConfiguration.class.getDeclaredField("sConfigurations");
+        field.setAccessible(true);
+        mConfigMap = (SparseArray<ViewConfiguration>) field.get(null);
+
+        mInitTracker = new InitTracker();
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = ZERO_BUTTON)
+    public void testSwipeUpFromApp() throws Exception {
+        try {
+            // Go to overview once so that all views are initialized and cached
+            startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+            mLauncher.getBackground().switchToOverview().dismissAllTasks();
+
+            // Track view creations
+            mInitTracker.startTracking();
+
+            startTestActivity(2);
+            mLauncher.getBackground().switchToOverview();
+
+            assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);
+        } finally {
+            mConfigMap.clear();
+        }
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = ZERO_BUTTON)
+    public void testSwipeUpFromApp_widget_update() {
+        String dummyText = "Some random dummy text";
+
+        executeSwipeUpTestWithWidget(
+                widgetId -> { },
+                widgetId -> AppWidgetManager.getInstance(getContext())
+                        .updateAppWidget(widgetId, createMainWidgetViews(dummyText)),
+                dummyText);
+    }
+
+    @Test
+    @NavigationModeSwitch(mode = ZERO_BUTTON)
+    public void testSwipeUp_with_list_widgets() {
+        SimpleViewsFactory viewFactory = new SimpleViewsFactory();
+        viewFactory.viewCount = 1;
+        Bundle args = new Bundle();
+        args.putBinder(EXTRA_VALUE, viewFactory.toBinder());
+        TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, args);
+
+        try {
+            executeSwipeUpTestWithWidget(
+                    widgetId -> {
+                        // Initialize widget
+                        RemoteViews views = createMainWidgetViews("List widget title");
+                        views.setRemoteAdapter(android.R.id.list,
+                                new Intent(getContext(), ListViewService.class));
+                        AppWidgetManager.getInstance(getContext()).updateAppWidget(widgetId, views);
+                        verifyWidget(viewFactory.getLabel(0));
+                    },
+                    widgetId -> {
+                        // Update widget
+                        viewFactory.viewCount = 2;
+                        AppWidgetManager.getInstance(getContext())
+                                .notifyAppWidgetViewDataChanged(widgetId, android.R.id.list);
+                    },
+                    viewFactory.getLabel(1)
+            );
+        } finally {
+            TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, new Bundle());
+        }
+    }
+
+    private void executeSwipeUpTestWithWidget(IntConsumer widgetIdCreationCallback,
+            IntConsumer updateBeforeSwipeUp, String finalWidgetText) {
+        try {
+            // Clear all existing data
+            LauncherSettings.Settings.call(mResolver,
+                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+            LauncherSettings.Settings.call(mResolver,
+                    LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+            LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
+            LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
+
+            addItemToScreen(item);
+            assertTrue("Widget is not present",
+                    mLauncher.pressHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
+            int widgetId = item.appWidgetId;
+
+            // Verify widget id
+            widgetIdCreationCallback.accept(widgetId);
+
+            // Go to overview once so that all views are initialized and cached
+            startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+            mLauncher.getBackground().switchToOverview().dismissAllTasks();
+
+            // Track view creations
+            mInitTracker.startTracking();
+
+            startTestActivity(2);
+            Background background = mLauncher.getBackground();
+
+            // Update widget
+            updateBeforeSwipeUp.accept(widgetId);
+
+            background.switchToOverview();
+            assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);
+
+            // Widget is updated when going home
+            mInitTracker.disableLog();
+            mLauncher.pressHome();
+            verifyWidget(finalWidgetText);
+            assertNotEquals(1, mInitTracker.viewInitCount);
+        } finally {
+            mConfigMap.clear();
+        }
+    }
+
+    private void verifyWidget(String text) {
+        assertNotNull("Widget not updated",
+                UiDevice.getInstance(getInstrumentation())
+                        .wait(Until.findObject(By.text(text)), DEFAULT_UI_TIMEOUT));
+    }
+
+    private RemoteViews createMainWidgetViews(String title) {
+        Context c = getContext();
+        int layoutId = c.getResources().getIdentifier(
+                "test_layout_widget_list", "layout", c.getPackageName());
+        RemoteViews views = new RemoteViews(c.getPackageName(), layoutId);
+        views.setTextViewText(android.R.id.text1, title);
+        return views;
+    }
+
+    private class InitTracker implements Answer {
+
+        public int viewInitCount = 0;
+
+        public boolean log = true;
+
+        @Override
+        public Object answer(InvocationOnMock invocation) throws Throwable {
+            Exception ex = new Exception();
+
+            boolean found = false;
+            for (StackTraceElement ste : ex.getStackTrace()) {
+                if ("<init>".equals(ste.getMethodName())
+                        && View.class.getName().equals(ste.getClassName())) {
+                    found = true;
+                    break;
+                }
+            }
+            if (found) {
+                viewInitCount++;
+                if (log) {
+                    Log.d("InitTracker", "New view inflated", ex);
+                }
+
+            }
+            return invocation.callRealMethod();
+        }
+
+        public void disableLog() {
+            log = false;
+        }
+
+        public void startTracking() {
+            ViewConfiguration vc = ViewConfiguration.get(mTargetContext);
+            ViewConfiguration spyVC = spy(vc);
+            mConfigMap.put(mConfigMap.keyAt(mConfigMap.indexOfValue(vc)), spyVC);
+            doAnswer(this).when(spyVC).getScaledTouchSlop();
+        }
+    }
+}
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
new file mode 100644
index 0000000..6ac36bf
--- /dev/null
+++ b/res/color/overview_button.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:alpha="1"
+        android:color="?attr/workspaceTextColor"
+        android:state_enabled="true" />
+    <item
+        android:alpha="?android:disabledAlpha"
+        android:color="?attr/workspaceTextColor"
+        android:state_enabled="false" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable-hdpi/work_tab_user_education.png b/res/drawable-hdpi/work_tab_user_education.png
deleted file mode 100644
index 1879dfb..0000000
--- a/res/drawable-hdpi/work_tab_user_education.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/work_tab_user_education.png b/res/drawable-mdpi/work_tab_user_education.png
deleted file mode 100644
index 65c7e63..0000000
--- a/res/drawable-mdpi/work_tab_user_education.png
+++ /dev/null
Binary files differ
diff --git a/go/quickstep/res/drawable/clear_all_button.xml b/res/drawable-v24/drag_handle_indicator_shadow.xml
similarity index 68%
copy from go/quickstep/res/drawable/clear_all_button.xml
copy to res/drawable-v24/drag_handle_indicator_shadow.xml
index acac32d..774bc38 100644
--- a/go/quickstep/res/drawable/clear_all_button.xml
+++ b/res/drawable-v24/drag_handle_indicator_shadow.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      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,
@@ -14,9 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape
+<com.android.launcher3.graphics.ShadowDrawable
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@color/clear_all_button_bg"/>
-    <corners android:radius="4dp"/>
-</shape>
+    android:src="@drawable/drag_handle_indicator_no_shadow"
+    android:elevation="@dimen/vertical_drag_handle_elevation" />
diff --git a/go/quickstep/res/drawable/clear_all_button.xml b/res/drawable-v24/ic_block_shadow.xml
similarity index 69%
copy from go/quickstep/res/drawable/clear_all_button.xml
copy to res/drawable-v24/ic_block_shadow.xml
index acac32d..045fe8d 100644
--- a/go/quickstep/res/drawable/clear_all_button.xml
+++ b/res/drawable-v24/ic_block_shadow.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      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,
@@ -14,9 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape
+<com.android.launcher3.graphics.ShadowDrawable
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@color/clear_all_button_bg"/>
-    <corners android:radius="4dp"/>
-</shape>
+    android:src="@drawable/ic_block_no_shadow"
+    android:elevation="@dimen/drop_target_shadow_elevation" />
diff --git a/res/drawable-xhdpi/work_tab_user_education.png b/res/drawable-xhdpi/work_tab_user_education.png
deleted file mode 100644
index 59df7a8..0000000
--- a/res/drawable-xhdpi/work_tab_user_education.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/work_tab_user_education.png b/res/drawable-xxhdpi/work_tab_user_education.png
deleted file mode 100644
index 3c6aa20..0000000
--- a/res/drawable-xxhdpi/work_tab_user_education.png
+++ /dev/null
Binary files differ
diff --git a/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml b/res/drawable/arrow_toast_rounded_background.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml
rename to res/drawable/arrow_toast_rounded_background.xml
diff --git a/res/drawable/bg_all_apps_button.xml b/res/drawable/bg_all_apps_button.xml
new file mode 100644
index 0000000..169a468
--- /dev/null
+++ b/res/drawable/bg_all_apps_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="oval" >
+            <solid android:color="#ffffffff" />
+        </shape>
+    </item>
+
+    <item>
+        <shape android:shape="oval" >
+            <solid android:color="?android:attr/colorAccent" />
+        </shape>
+    </item>
+</ripple>
diff --git a/go/quickstep/res/drawable/empty_content_box.xml b/res/drawable/bottom_sheet_top_border.xml
similarity index 61%
rename from go/quickstep/res/drawable/empty_content_box.xml
rename to res/drawable/bottom_sheet_top_border.xml
index a488388..23f4e51 100644
--- a/go/quickstep/res/drawable/empty_content_box.xml
+++ b/res/drawable/bottom_sheet_top_border.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -14,10 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/transparent"/>
-    <stroke android:color="@android:color/white" android:width="4px"/>
-    <corners android:radius="2dp"/>
-</shape>
\ No newline at end of file
+<vector android:height="15.53398dp" android:viewportHeight="32"
+    android:viewportWidth="412" android:width="200dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M412,32v-2.64C349.26,10.51 279.5,0 206,0S62.74,10.51 0,29.36V32H412z"/>
+</vector>
diff --git a/res/drawable/drag_handle_indicator.xml b/res/drawable/drag_handle_indicator.xml
deleted file mode 100644
index b01b84a..0000000
--- a/res/drawable/drag_handle_indicator.xml
+++ /dev/null
@@ -1,40 +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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/vertical_drag_handle_size"
-    android:height="@dimen/vertical_drag_handle_size"
-    android:viewportWidth="36.0"
-    android:viewportHeight="36.0" >
-
-    <group
-        android:translateX="11.5"
-        android:translateY="11.5">
-        <path
-            android:pathData="M2 8.5L6.5 4L11 8.5"
-            android:strokeColor="?attr/workspaceAmbientShadowColor"
-            android:strokeWidth="3.6"
-            android:strokeLineCap="round"
-            android:strokeLineJoin="round" />
-
-        <path
-            android:pathData="M2 8.5L6.5 4L11 8.5"
-            android:strokeColor="?attr/workspaceTextColor"
-            android:strokeWidth="1.8"
-            android:strokeLineCap="round"
-            android:strokeLineJoin="round" />
-        </group>
-</vector>
diff --git a/res/drawable/drag_handle_indicator_no_shadow.xml b/res/drawable/drag_handle_indicator_no_shadow.xml
new file mode 100644
index 0000000..341e60c
--- /dev/null
+++ b/res/drawable/drag_handle_indicator_no_shadow.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/vertical_drag_handle_width"
+    android:height="@dimen/vertical_drag_handle_height"
+    android:viewportWidth="18.0"
+    android:viewportHeight="6.0"
+    android:tint="?attr/workspaceTextColor" >
+
+    <path
+        android:pathData="M17,6c-0.15,0-0.3-0.03-0.45-0.11L9,2.12L1.45,5.89c-0.5,0.25-1.09,
+        0.05-1.34-0.45S0.06,4.35,0.55,4.11l8-4c0.28-0.14,0.61-0.14,0.89,0l8,4c0.49,0.25,0.69,
+        0.85,0.45,1.34C17.72,5.8,17.37,6,17,6z"
+        android:fillColor="@android:color/white" />
+</vector>
diff --git a/res/drawable/gesture_tutorial_ripple.xml b/res/drawable/gesture_tutorial_ripple.xml
new file mode 100644
index 0000000..782af33
--- /dev/null
+++ b/res/drawable/gesture_tutorial_ripple.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple android:color="@color/gesture_tutorial_ripple_color"
+    xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file
diff --git a/SecondaryDisplayLauncher/res/drawable/ic_apps.xml b/res/drawable/ic_apps.xml
similarity index 100%
rename from SecondaryDisplayLauncher/res/drawable/ic_apps.xml
rename to res/drawable/ic_apps.xml
diff --git a/res/drawable/ic_block_no_shadow.xml b/res/drawable/ic_block_no_shadow.xml
new file mode 100644
index 0000000..edeb4c6
--- /dev/null
+++ b/res/drawable/ic_block_no_shadow.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:tint="?android:attr/textColorPrimary">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M4,12c0-4.42,3.58-8,8-8c1.85,0,3.55,0.63,4.9,1.69
+	L5.69,16.9C4.63,15.55,4,13.85,4,12z M12,20c-1.85,0-3.55-0.63-4.9-1.69L18.31,7.1C19.37,8.45,20,10.15,20,12
+	C20,16.42,16.42,20,12,20z" />
+</vector>
diff --git a/res/drawable/ic_corp.xml b/res/drawable/ic_corp.xml
index b59113d..76dccd3 100644
--- a/res/drawable/ic_corp.xml
+++ b/res/drawable/ic_corp.xml
@@ -19,9 +19,5 @@
     android:viewportWidth="24.0"
     android:viewportHeight="24.0"
     android:tint="?android:attr/textColorHint" >
-    <path
-        android:pathData="M20 6h-4V4c0-1.11-0.89-2-2-2h-4c-1.11 0-2 0.89-2 2v2H4c-1.11 0-1.99 0.89 -1.99
-2L2 19c0 1.11 0.89 2 2 2h16c1.11 0 2-0.89 2-2V8c0-1.11-0.89-2-2-2zM10
-4h4v2h-4V4zm10 15H4V8h16v11z"
-        android:fillColor="@android:color/white"/>
+    <path android:fillColor="@android:color/white" android:pathData="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-8 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm2-9h-4V4h4v2z"/>
 </vector>
\ No newline at end of file
diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml
new file mode 100644
index 0000000..62a9787
--- /dev/null
+++ b/res/drawable/ic_corp_off.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/textColorHint" >
+    <path
+        android:pathData="M22 7.95c.05-1.11-.84-2-1.95-1.95H16V3.95c0-1.11-.84-2-1.95-1.95h-4C8.94 1.95 8 2.84 8 3.95v.32l14 14V7.95zM14 6h-4V4h4v2zm7.54 14.28l-7.56-7.56v.01l-1.7-1.7h.01L7.21 5.95 3.25 1.99 1.99 3.27 4.69 6h-.64c-1.11 0-1.99.86-1.99 1.97l-.01 11.02c0 1.11.89 2.01 2 2.01h15.64l2.05 2.02L23 21.75l-1.46-1.47z"
+        android:fillColor="@android:color/white"/>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_pin.xml b/res/drawable/ic_pin.xml
similarity index 100%
rename from quickstep/res/drawable/ic_pin.xml
rename to res/drawable/ic_pin.xml
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 9427ae0..67ec664 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -16,9 +16,10 @@
 <!-- The top and bottom paddings are defined in this container, but since we want
      the list view to span the full width (for touch interception purposes), we
      will bake the left/right padding into that view's background itself. -->
-<com.android.launcher3.allapps.AllAppsContainerView
+<com.android.launcher3.allapps.LauncherAllAppsContainerView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/apps_view"
+    android:theme="?attr/allAppsTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="true"
@@ -57,7 +58,6 @@
                 android:layout_weight="1"
                 android:background="?android:attr/selectableItemBackground"
                 android:text="@string/all_apps_personal_tab"
-                android:textAllCaps="true"
                 android:textColor="@color/all_apps_tab_text"
                 android:textSize="14sp" />
 
@@ -68,7 +68,6 @@
                 android:layout_weight="1"
                 android:background="?android:attr/selectableItemBackground"
                 android:text="@string/all_apps_work_tab"
-                android:textAllCaps="true"
                 android:textColor="@color/all_apps_tab_text"
                 android:textSize="14sp" />
         </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
@@ -79,4 +78,4 @@
         layout="@layout/search_container_all_apps"/>
 
     <include layout="@layout/all_apps_fast_scroller" />
-</com.android.launcher3.allapps.AllAppsContainerView>
\ No newline at end of file
+</com.android.launcher3.allapps.LauncherAllAppsContainerView>
\ No newline at end of file
diff --git a/SecondaryDisplayLauncher/res/layout/app_grid_item.xml b/res/layout/all_apps_content_layout.xml
similarity index 61%
rename from SecondaryDisplayLauncher/res/layout/app_grid_item.xml
rename to res/layout/all_apps_content_layout.xml
index ee5158a..5698977 100644
--- a/SecondaryDisplayLauncher/res/layout/app_grid_item.xml
+++ b/res/layout/all_apps_content_layout.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2018 The Android Open Source Project
+  ~ Copyright (C) 2020 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,22 +14,14 @@
   ~ 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:orientation="vertical"
+    android:id="@+id/apps_list_view_override"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:gravity="center">
-
-    <ImageView
-        android:id="@+id/app_icon"
-        android:layout_width="@dimen/app_icon_width"
-        android:layout_height="@dimen/app_icon_height" />
-
-    <TextView
-        android:id="@+id/app_name"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        android:maxLines="1" />
+    android:layout_below="@id/search_container_all_apps"
+    android:clipToPadding="false"
+    android:descendantFocusability="afterDescendants"
+    android:focusable="true"
+    android:layout_marginTop="@dimen/all_apps_header_top_padding"
+    android:orientation="vertical">
 </LinearLayout>
diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/res/layout/arrow_toast.xml
similarity index 92%
rename from quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
rename to res/layout/arrow_toast.xml
index b0f2b4b..0ec9981 100644
--- a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
+++ b/res/layout/arrow_toast.xml
@@ -34,6 +34,9 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
+            android:paddingTop="5dp"
+            android:paddingBottom="5dp"
+            android:gravity="center"
             android:layout_gravity="center_vertical"
             android:textColor="@android:color/white"
             android:textSize="16sp"/>
@@ -49,7 +52,7 @@
             android:src="@drawable/ic_remove_no_shadow"
             android:tint="@android:color/white"
             android:background="?android:attr/selectableItemBackgroundBorderless"
-            android:contentDescription="@string/accessibility_close_task"/>
+            android:contentDescription="@string/accessibility_close"/>
     </LinearLayout>
 
     <View
@@ -57,6 +60,5 @@
         android:elevation="2dp"
         android:layout_width="10dp"
         android:layout_height="8dp"
-        android:layout_marginTop="-2dp"
-        android:layout_gravity="center_horizontal"/>
+        android:layout_marginTop="-2dp"/>
 </merge>
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index cca899b..a137908 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -45,15 +45,15 @@
 
         <include
             android:id="@+id/overview_panel"
-            layout="@layout/overview_panel"
-            android:visibility="gone" />
+            layout="@layout/overview_panel" />
+
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
         <com.android.launcher3.pageindicators.WorkspacePageIndicator
             android:id="@+id/page_indicator"
             android:layout_width="match_parent"
-            android:layout_height="@dimen/vertical_drag_handle_size"
+            android:layout_height="@dimen/workspace_page_indicator_height"
             android:layout_gravity="bottom|center_horizontal"
             android:theme="@style/HomeScreenElementTheme" />
 
diff --git a/go/quickstep/res/drawable/clear_all_button.xml b/res/layout/overview_actions_container.xml
similarity index 77%
rename from go/quickstep/res/drawable/clear_all_button.xml
rename to res/layout/overview_actions_container.xml
index acac32d..5946bf6 100644
--- a/go/quickstep/res/drawable/clear_all_button.xml
+++ b/res/layout/overview_actions_container.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2019 The Android Open Source Project
+     Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -14,9 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape
+<Space
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@color/clear_all_button_bg"/>
-    <corners android:radius="4dp"/>
-</shape>
+    android:layout_width="0dp"
+    android:layout_height="0dp" />
diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml
index bdd5d23..f513688 100644
--- a/res/layout/overview_panel.xml
+++ b/res/layout/overview_panel.xml
@@ -15,6 +15,7 @@
      limitations under the License.
 -->
 <Space
-      xmlns:android="http://schemas.android.com/apk/res/android"
-      android:layout_width="0dp"
-      android:layout_height="0dp" />
\ No newline at end of file
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dp"
+    android:layout_height="0dp"
+    android:visibility="gone" />
\ No newline at end of file
diff --git a/res/layout/search_container_all_apps.xml b/res/layout/search_container_all_apps.xml
index fd9cb60..e1646ba 100644
--- a/res/layout/search_container_all_apps.xml
+++ b/res/layout/search_container_all_apps.xml
@@ -34,5 +34,4 @@
     android:singleLine="true"
     android:textColor="?android:attr/textColorSecondary"
     android:textColorHint="@drawable/all_apps_search_hint"
-    android:textSize="16sp"
-    android:translationY="24dp" />
\ No newline at end of file
+    android:textSize="16sp" />
\ No newline at end of file
diff --git a/res/layout/secondary_launcher.xml b/res/layout/secondary_launcher.xml
new file mode 100644
index 0000000..fdf4446
--- /dev/null
+++ b/res/layout/secondary_launcher.xml
@@ -0,0 +1,127 @@
+<?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.secondarydisplay.SecondaryDragLayer
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/drag_layer"
+    android:padding="@dimen/dynamic_grid_edge_margin">
+
+    <GridView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="100dp"
+        android:theme="@style/HomeScreenElementTheme"
+        android:layout_gravity="center_horizontal|top"
+        android:layout_margin="@dimen/dynamic_grid_edge_margin"
+        android:id="@+id/workspace_grid" />
+
+    <ImageButton
+        android:id="@+id/all_apps_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_margin="40dp"
+        android:padding="16dp"
+        android:src="@drawable/ic_apps"
+        android:background="@drawable/bg_all_apps_button"
+        android:contentDescription="@string/all_apps_button_label"
+        android:onClick="onAppsButtonClicked" />
+
+    <com.android.launcher3.allapps.AllAppsContainerView
+        android:id="@+id/apps_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="true"
+        android:clipToPadding="false"
+        android:focusable="false"
+        android:saveEnabled="false"
+        android:layout_gravity="bottom|end"
+        android:background="@drawable/round_rect_primary"
+        android:elevation="2dp"
+        android:visibility="invisible" >
+
+        <include
+            layout="@layout/all_apps_rv_layout"
+            android:visibility="gone" />
+
+        <com.android.launcher3.allapps.FloatingHeaderView
+            android:id="@+id/all_apps_header"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/search_container_all_apps"
+            android:clipToPadding="false"
+            android:paddingTop="@dimen/all_apps_header_top_padding"
+            android:orientation="vertical" >
+
+            <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:textAllCaps="true"
+                    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:textAllCaps="true"
+                    android:textColor="@color/all_apps_tab_text"
+                    android:textSize="14sp" />
+            </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
+        </com.android.launcher3.allapps.FloatingHeaderView>
+
+        <com.android.launcher3.allapps.search.AppsSearchContainerLayout
+            android:id="@id/search_container_all_apps"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/all_apps_search_bar_field_height"
+            android:layout_centerHorizontal="true"
+            android:layout_gravity="top|center_horizontal"
+            android:background="@drawable/bg_all_apps_searchbox"
+            android:elevation="1dp"
+            android:focusableInTouchMode="true"
+            android:gravity="center"
+            android:hint="@string/all_apps_search_bar_hint"
+            android:imeOptions="actionSearch|flagNoExtractUi"
+            android:inputType="text|textNoSuggestions|textCapWords"
+            android:maxLines="1"
+            android:padding="8dp"
+            android:saveEnabled="false"
+            android:scrollHorizontally="true"
+            android:singleLine="true"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textColorHint="@drawable/all_apps_search_hint"
+            android:textSize="16sp" />
+
+        <include layout="@layout/all_apps_fast_scroller" />
+    </com.android.launcher3.allapps.AllAppsContainerView>
+</com.android.launcher3.secondarydisplay.SecondaryDragLayer>
\ No newline at end of file
diff --git a/res/layout/system_shortcut.xml b/res/layout/system_shortcut.xml
index 4b7097a..881df1b 100644
--- a/res/layout/system_shortcut.xml
+++ b/res/layout/system_shortcut.xml
@@ -22,7 +22,7 @@
     android:theme="@style/PopupItem" >
 
     <com.android.launcher3.BubbleTextView
-        style="@style/BaseIcon"
+        style="@style/BaseIconUnBounded"
         android:id="@+id/bubble_text"
         android:background="?android:attr/selectableItemBackground"
         android:gravity="start|center_vertical"
@@ -30,6 +30,7 @@
         android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
         android:paddingEnd="@dimen/popup_padding_end"
         android:textSize="14sp"
+        android:maxLines="2"
         android:textColor="?android:attr/textColorPrimary"
         launcher:iconDisplay="shortcut_popup"
         launcher:layoutHorizontal="true"
diff --git a/res/layout/system_shortcut_icon_only.xml b/res/layout/system_shortcut_icon_only.xml
index b8b5b8c..5a81f70 100644
--- a/res/layout/system_shortcut_icon_only.xml
+++ b/res/layout/system_shortcut_icon_only.xml
@@ -19,7 +19,7 @@
     android:layout_width="@dimen/system_shortcut_header_icon_touch_size"
     android:layout_height="@dimen/system_shortcut_header_icon_touch_size"
     android:background="?android:attr/selectableItemBackgroundBorderless"
-    android:tint="?android:attr/textColorHint"
+    android:tint="?attr/iconOnlyShortcutColor"
     android:tintMode="src_in"
     android:padding="@dimen/system_shortcut_header_icon_padding"
     android:theme="@style/PopupItem" />
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index a1033f0..923352e 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -41,14 +41,14 @@
         android:paddingLeft="12dp"
         android:paddingRight="12dp" >
 
-        <com.android.launcher3.ExtendedEditText
+        <com.android.launcher3.folder.FolderNameEditText
             android:id="@+id/folder_name"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
+            style="@style/TextHeadline"
             android:layout_weight="1"
             android:background="@android:color/transparent"
-            android:fontFamily="sans-serif-condensed"
             android:textStyle="bold"
             android:gravity="center_horizontal"
             android:hint="@string/folder_hint_text"
@@ -58,7 +58,7 @@
             android:singleLine="true"
             android:textColor="?attr/folderTextColor"
             android:textColorHighlight="?android:attr/colorControlHighlight"
-            android:textColorHint="?attr/folderTextColor"
+            android:textColorHint="?attr/folderHintColor"
             android:textSize="@dimen/folder_label_text_size" />
 
         <com.android.launcher3.pageindicators.PageIndicatorDots
diff --git a/res/layout/work_apps_paused.xml b/res/layout/work_apps_paused.xml
new file mode 100644
index 0000000..7f1107f
--- /dev/null
+++ b/res/layout/work_apps_paused.xml
@@ -0,0 +1,43 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/allAppsScrimColor"
+    android:padding="48dp"
+    android:orientation="vertical"
+    android:gravity="center">
+
+    <TextView
+        style="@style/PrimaryHeadline"
+        android:textColor="?attr/workProfileOverlayTextColor"
+        android:id="@+id/work_apps_paused_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="8dp"
+        android:text="@string/work_apps_paused_title"
+        android:textAlignment="center"
+        android:textSize="20sp" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/work_apps_paused_content"
+        android:textColor="?attr/workProfileOverlayTextColor"
+        android:text="@string/work_apps_paused_body"
+        android:textAlignment="center"
+        android:textSize="16sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
new file mode 100644
index 0000000..31953c7
--- /dev/null
+++ b/res/layout/work_mode_switch.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.allapps.WorkModeSwitch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="@style/PrimaryHeadline"
+    android:id="@+id/work_mode_toggle"
+    android:drawableStart="@drawable/ic_corp"
+    android:drawablePadding="16dp"
+    android:drawableTint="?attr/workProfileOverlayTextColor"
+    android:textColor="?attr/workProfileOverlayTextColor"
+    android:layout_alignParentBottom="true"
+    android:ellipsize="end"
+    android:elevation="10dp"
+    android:gravity="start"
+    android:lines="1"
+    android:showText="false"
+    android:textSize="@dimen/work_profile_footer_text_size"
+    android:background="?attr/allAppsScrimColor"
+    android:text="@string/work_profile_toggle_label"
+    android:paddingBottom="@dimen/work_profile_footer_padding"
+    android:paddingLeft="@dimen/work_profile_footer_padding"
+    android:paddingRight="@dimen/work_profile_footer_padding"
+    android:paddingTop="@dimen/work_profile_footer_padding"
+/>
diff --git a/res/layout/work_profile_edu.xml b/res/layout/work_profile_edu.xml
new file mode 100644
index 0000000..c3c7010
--- /dev/null
+++ b/res/layout/work_profile_edu.xml
@@ -0,0 +1,61 @@
+<?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.views.WorkEduView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    android:gravity="bottom"
+    android:orientation="vertical">
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="32dp"
+        android:background="@drawable/bottom_sheet_top_border"
+        android:backgroundTint="?attr/eduHalfSheetBGColor" />
+
+    <LinearLayout
+        android:id="@+id/view_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="?attr/eduHalfSheetBGColor"
+        android:orientation="vertical"
+        android:paddingLeft="@dimen/bottom_sheet_edu_padding"
+        android:paddingRight="@dimen/bottom_sheet_edu_padding">
+
+        <TextView
+            android:id="@+id/content_text"
+            style="@style/TextHeadline"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="48dp"
+            android:layout_marginBottom="48dp"
+            android:gravity="center"
+            android:text="@string/work_profile_edu_personal_apps"
+            android:textAlignment="center"
+            android:textColor="@android:color/white"
+            android:textSize="20sp" />
+
+        <Button
+            android:id="@+id/proceed"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:layout_gravity="end"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center"
+            android:text="@string/work_profile_edu_next"
+            android:textAlignment="center"
+            android:textColor="@android:color/white" />
+    </LinearLayout>
+</com.android.launcher3.views.WorkEduView>
\ No newline at end of file
diff --git a/res/layout/work_tab_bottom_user_education_view.xml b/res/layout/work_tab_bottom_user_education_view.xml
deleted file mode 100644
index ac2deeb..0000000
--- a/res/layout/work_tab_bottom_user_education_view.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.views.BottomUserEducationView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_gravity="bottom"
-    android:background="?android:attr/colorAccent"
-    android:elevation="2dp"
-    android:focusable="true"
-    android:orientation="horizontal">
-
-  <ImageView
-      android:layout_width="134dp"
-      android:layout_height="134dp"
-      android:layout_marginTop="28dp"
-      android:layout_marginLeft="20dp"
-      android:src="@drawable/work_tab_user_education"/>
-
-  <LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:layout_marginStart="24dp"
-      android:orientation="vertical">
-
-    <ImageView
-        android:id="@+id/close_bottom_user_tip"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:layout_marginTop="12dp"
-        android:layout_marginEnd="12dp"
-        android:layout_gravity="right"
-        android:contentDescription="@string/bottom_work_tab_user_education_close_button"
-        android:src="@drawable/ic_remove_no_shadow"/>
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="4dp"
-        android:layout_marginEnd="24dp"
-        android:fontFamily="roboto-medium"
-        android:text="@string/bottom_work_tab_user_education_title"
-        android:textColor="@android:color/white"
-        android:textSize="20sp"/>
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="24dp"
-        android:text="@string/bottom_work_tab_user_education_body"
-        android:textColor="@android:color/white"
-        android:textSize="14sp"/>
-
-  </LinearLayout>
-
-</com.android.launcher3.views.BottomUserEducationView>
\ No newline at end of file
diff --git a/res/layout/work_tab_footer.xml b/res/layout/work_tab_footer.xml
deleted file mode 100644
index 379e9d0..0000000
--- a/res/layout/work_tab_footer.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.views.WorkFooterContainer
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:focusable="true"
-    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_bottom_padding"
-    android:paddingLeft="@dimen/dynamic_grid_cell_padding_x"
-    android:paddingRight="@dimen/dynamic_grid_cell_padding_x"
-    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_top_padding">
-
-    <ImageView
-        android:id="@+id/work_footer_divider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:focusable="false"
-        android:importantForAccessibility="no"
-        android:paddingBottom="@dimen/all_apps_divider_margin_vertical"
-        android:paddingTop="@dimen/all_apps_divider_margin_vertical"
-        android:scaleType="fitXY"
-        android:src="@drawable/all_apps_divider"/>
-
-    <com.android.launcher3.allapps.WorkModeSwitch
-        android:id="@+id/work_mode_toggle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentEnd="true"
-        android:layout_below="@id/work_footer_divider"/>
-
-    <TextView
-        android:id="@android:id/title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignBaseline="@id/work_mode_toggle"
-        android:layout_alignParentStart="true"
-        android:ellipsize="end"
-        android:lines="1"
-        android:text="@string/work_profile_toggle_label"
-        android:textColor="?android:attr/textColorTertiary"
-        android:textSize="16sp"/>
-
-    <ImageView
-        android:id="@android:id/icon"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:layout_below="@android:id/title"
-        android:layout_marginTop="8dp"
-        android:src="@drawable/ic_corp"/>
-
-    <TextView
-        android:id="@+id/managed_by_label"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_below="@android:id/title"
-        android:layout_marginTop="8dp"
-        android:layout_toEndOf="@android:id/icon"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:lines="1"
-        android:minHeight="24dp"
-        android:paddingStart="12dp"
-        android:textColor="?android:attr/textColorHint"
-        android:textSize="13sp"/>
-
-</com.android.launcher3.views.WorkFooterContainer>
\ No newline at end of file
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index ba16dd3..ca59afa 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Werk"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Program is nie geïnstalleer nie."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Program is nie beskikbaar nie"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 2fbdae1..a80ecb0 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"العمل"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"لم يتم تثبيت التطبيق."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"التطبيق ليس متاحًا"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 221921f..2984603 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"কৰ্মস্থান"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"এপটো ইনষ্টল কৰা নহ\'ল।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"এপটো নাই"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index e6fe3bd..883003c 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija nije instalirana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija nije dostupna"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 2ccd34f..b4cf913 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Працоўная"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Праграма не ўсталявана."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Праграма недаступная"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index f7d1d0f..408f205 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Работа"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Приложението не е инсталирано."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Приложението не е налично"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 0a72632..09608b5 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -92,7 +92,7 @@
     <string name="title_change_settings" msgid="1376365968844349552">"সেটিংস পরিবর্তন করুন"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"বিজ্ঞপ্তির ডট দেখুন"</string>
     <string name="auto_add_shortcuts_label" msgid="8222286205987725611">"হোম স্ক্রিনে আইকন যোগ করুন"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"নতুন অ্যাপের জন্য"</string>
+    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"নতুন অ্যাপ্লিকেশানগুলির জন্যে"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"অজানা"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"সরান"</string>
     <string name="abandoned_search" msgid="891119232568284442">"সার্চ"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 57c5072..692b57d 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Práce"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikace není nainstalována."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikace není k dispozici."</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 9097ffd..dc17516 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbejde"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen er ikke installeret."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen er ikke tilgængelig"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 1602a53..a345bab 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbeit"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App ist nicht installiert."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App nicht verfügbar"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
deleted file mode 100644
index 7adc218..0000000
--- a/res/values-en-rCA/strings.xml
+++ /dev/null
@@ -1,142 +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.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
-    <string name="activity_not_found" msgid="8071924732094499514">"App isn\'t installed."</string>
-    <string name="activity_not_available" msgid="7456344436509528827">"App isn\'t available"</string>
-    <string name="safemode_shortcut_error" msgid="9160126848219158407">"Downloaded app disabled in Safe mode"</string>
-    <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
-    <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
-    <string name="home_screen" msgid="806512411299847073">"Home screen"</string>
-    <string name="custom_actions" msgid="3747508247759093328">"Customised actions"</string>
-    <string name="long_press_widget_to_add" msgid="7699152356777458215">"Touch &amp; hold to pick up a widget."</string>
-    <string name="long_accessible_way_to_add" msgid="4289502106628154155">"Double-tap &amp; hold to pick up a widget or use customised actions."</string>
-    <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
-    <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d wide by %2$d high"</string>
-    <string name="add_item_request_drag_hint" msgid="5899764264480397019">"Touch &amp; hold to place manually"</string>
-    <string name="place_automatically" msgid="8064208734425456485">"Add automatically"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
-    <string name="all_apps_loading_message" msgid="5813968043155271636">"Loading apps…"</string>
-    <string name="all_apps_no_search_results" msgid="3200346862396363786">"No apps found matching \'<xliff:g id="QUERY">%1$s</xliff:g>\'"</string>
-    <string name="all_apps_search_market_message" msgid="1366263386197059176">"Search for more apps"</string>
-    <string name="label_application" msgid="8531721983832654978">"App"</string>
-    <string name="notifications_header" msgid="1404149926117359025">"Notifications"</string>
-    <string name="long_press_shortcut_to_add" msgid="4524750017792716791">"Touch &amp; hold to pick up a shortcut."</string>
-    <string name="long_accessible_way_to_add_shortcut" msgid="3327314059613154633">"Double-tap &amp; hold to pick up a shortcut or use custom actions."</string>
-    <string name="out_of_space" msgid="4691004494942118364">"No more room on this Home screen."</string>
-    <string name="hotseat_out_of_space" msgid="7448809638125333693">"No more room in the Favourites tray"</string>
-    <string name="all_apps_button_label" msgid="8130441508702294465">"Apps list"</string>
-    <string name="all_apps_button_personal_label" msgid="1315764287305224468">"Personal apps list"</string>
-    <string name="all_apps_button_work_label" msgid="7270707118948892488">"Work apps list"</string>
-    <string name="all_apps_home_button_label" msgid="252062713717058851">"Home"</string>
-    <string name="remove_drop_target_label" msgid="7812859488053230776">"Remove"</string>
-    <string name="uninstall_drop_target_label" msgid="4722034217958379417">"Uninstall"</string>
-    <string name="app_info_drop_target_label" msgid="692894985365717661">"App info"</string>
-    <string name="install_drop_target_label" msgid="2539096853673231757">"Install"</string>
-    <string name="permlab_install_shortcut" msgid="5632423390354674437">"install shortcuts"</string>
-    <string name="permdesc_install_shortcut" msgid="923466509822011139">"Allows an app to add shortcuts without user intervention."</string>
-    <string name="permlab_read_settings" msgid="1941457408239617576">"read Home settings and shortcuts"</string>
-    <string name="permdesc_read_settings" msgid="5833423719057558387">"Allows the app to read the settings and shortcuts in Home."</string>
-    <string name="permlab_write_settings" msgid="3574213698004620587">"write Home settings and shortcuts"</string>
-    <string name="permdesc_write_settings" msgid="5440712911516509985">"Allows the app to change the settings and shortcuts in Home."</string>
-    <string name="msg_no_phone_permission" msgid="9208659281529857371">"<xliff:g id="APP_NAME">%1$s</xliff:g> is not allowed to make phone calls"</string>
-    <string name="gadget_error_text" msgid="6081085226050792095">"Problem loading widget"</string>
-    <string name="gadget_setup_text" msgid="8274003207686040488">"Setup"</string>
-    <string name="uninstall_system_app_text" msgid="4172046090762920660">"This is a system app and can\'t be uninstalled."</string>
-    <string name="folder_hint_text" msgid="6617836969016293992">"Unnamed Folder"</string>
-    <string name="disabled_app_label" msgid="6673129024321402780">"Disabled <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
-    <plurals name="dotted_app_label" formatted="false" msgid="5194538107138265416">
-      <item quantity="other"><xliff:g id="APP_NAME_2">%1$s</xliff:g>, has <xliff:g id="NOTIFICATION_COUNT_3">%2$d</xliff:g> notifications</item>
-      <item quantity="one"><xliff:g id="APP_NAME_0">%1$s</xliff:g>, has <xliff:g id="NOTIFICATION_COUNT_1">%2$d</xliff:g> notification</item>
-    </plurals>
-    <string name="default_scroll_format" msgid="7475544710230993317">"Page %1$d of %2$d"</string>
-    <string name="workspace_scroll_format" msgid="8458889198184077399">"Home screen %1$d of %2$d"</string>
-    <string name="workspace_new_page" msgid="257366611030256142">"New home screen page"</string>
-    <string name="folder_opened" msgid="94695026776264709">"Folder opened, <xliff:g id="WIDTH">%1$d</xliff:g> by <xliff:g id="HEIGHT">%2$d</xliff:g>"</string>
-    <string name="folder_tap_to_close" msgid="4625795376335528256">"Tap to close folder"</string>
-    <string name="folder_tap_to_rename" msgid="4017685068016979677">"Tap to save rename"</string>
-    <string name="folder_closed" msgid="4100806530910930934">"Folder closed"</string>
-    <string name="folder_renamed" msgid="1794088362165669656">"Folder renamed to <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="folder_name_format" msgid="6629239338071103179">"Folder: <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="widget_button_text" msgid="2880537293434387943">"Widgets"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpapers"</string>
-    <string name="styles_wallpaper_button_text" msgid="4342122323125579619">"Styles &amp; wallpapers"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"Home settings"</string>
-    <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
-    <string name="allow_rotation_title" msgid="7728578836261442095">"Allow Home screen rotation"</string>
-    <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
-    <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
-    <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
-    <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
-    <string name="title_missing_notification_access" msgid="7503287056163941064">"Notification access needed"</string>
-    <string name="msg_missing_notification_access" msgid="281113995110910548">"To show Notification Dots, turn on app notifications for <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="title_change_settings" msgid="1376365968844349552">"Change settings"</string>
-    <string name="notification_dots_service_title" msgid="4284221181793592871">"Show notification dots"</string>
-    <string name="auto_add_shortcuts_label" msgid="8222286205987725611">"Add icon to Home screen"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"For new apps"</string>
-    <string name="package_state_unknown" msgid="7592128424511031410">"Unknown"</string>
-    <string name="abandoned_clean_this" msgid="7610119707847920412">"Remove"</string>
-    <string name="abandoned_search" msgid="891119232568284442">"Search"</string>
-    <string name="abandoned_promises_title" msgid="7096178467971716750">"This app is not installed"</string>
-    <string name="abandoned_promise_explanation" msgid="3990027586878167529">"The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually."</string>
-    <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
-    <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="widgets_bottom_sheet_title" msgid="2904559530954183366">"<xliff:g id="NAME">%1$s</xliff:g> widgets"</string>
-    <string name="widgets_list" msgid="796804551140113767">"Widgets list"</string>
-    <string name="widgets_list_closed" msgid="6141506579418771922">"Widgets list closed"</string>
-    <string name="action_add_to_workspace" msgid="8902165848117513641">"Add to Home screen"</string>
-    <string name="action_move_here" msgid="2170188780612570250">"Move item here"</string>
-    <string name="item_added_to_workspace" msgid="4211073925752213539">"Item added to home screen"</string>
-    <string name="item_removed" msgid="851119963877842327">"Item removed"</string>
-    <string name="undo" msgid="4151576204245173321">"Undo"</string>
-    <string name="action_move" msgid="4339390619886385032">"Move item"</string>
-    <string name="move_to_empty_cell" msgid="2833711483015685619">"Move to row <xliff:g id="NUMBER_0">%1$s</xliff:g> column <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
-    <string name="move_to_position" msgid="6750008980455459790">"Move to position <xliff:g id="NUMBER">%1$s</xliff:g>"</string>
-    <string name="move_to_hotseat_position" msgid="6295412897075147808">"Move to favourites position <xliff:g id="NUMBER">%1$s</xliff:g>"</string>
-    <string name="item_moved" msgid="4606538322571412879">"Item moved"</string>
-    <string name="add_to_folder" msgid="9040534766770853243">"Add to folder: <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="add_to_folder_with_app" msgid="4534929978967147231">"Add to folder with <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="added_to_folder" msgid="4793259502305558003">"Item added to folder"</string>
-    <string name="create_folder_with" msgid="4050141361160214248">"Create folder with: <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="folder_created" msgid="6409794597405184510">"Folder created"</string>
-    <string name="action_move_to_workspace" msgid="1603837886334246317">"Move to Home screen"</string>
-    <string name="action_resize" msgid="1802976324781771067">"Re-size"</string>
-    <string name="action_increase_width" msgid="8773715375078513326">"Increase width"</string>
-    <string name="action_increase_height" msgid="459390020612501122">"Increase height"</string>
-    <string name="action_decrease_width" msgid="1374549771083094654">"Decrease width"</string>
-    <string name="action_decrease_height" msgid="282377193880900022">"Decrease height"</string>
-    <string name="widget_resized" msgid="9130327887929620">"Widget re-sized to width <xliff:g id="NUMBER_0">%1$s</xliff:g> height <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
-    <string name="action_deep_shortcut" msgid="2864038805849372848">"Short cuts"</string>
-    <string name="shortcuts_menu_with_notifications_description" msgid="2676582286544232849">"Shortcuts and notifications"</string>
-    <string name="action_dismiss_notification" msgid="5909461085055959187">"Dismiss"</string>
-    <string name="notification_dismissed" msgid="6002233469409822874">"Notification dismissed"</string>
-    <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personal"</string>
-    <string name="all_apps_work_tab" msgid="4884822796154055118">"Work"</string>
-    <string name="work_profile_toggle_label" msgid="3081029915775481146">"Work profile"</string>
-    <string name="bottom_work_tab_user_education_title" msgid="5785851780786322825">"Find work apps here"</string>
-    <string name="bottom_work_tab_user_education_body" msgid="2818107472360579152">"Each work app has a badge and is kept secure by your organisation. Move apps to your Home screen for easier access."</string>
-    <string name="work_mode_on_label" msgid="4781128097185272916">"Managed by your organisation"</string>
-    <string name="work_mode_off_label" msgid="3194894777601421047">"Notifications and apps are off"</string>
-    <string name="bottom_work_tab_user_education_close_button" msgid="4224492243977802135">"Close"</string>
-    <string name="bottom_work_tab_user_education_closed" msgid="1098340939861869465">"Closed"</string>
-    <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-</resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
deleted file mode 100644
index c741482..0000000
--- a/res/values-en-rXC/strings.xml
+++ /dev/null
@@ -1,142 +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.
-*/
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_name" msgid="649227358658669779">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‏‏‏‏‏‎‎‏‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‎‏‎‎‏‏‎Launcher3‎‏‎‎‏‎"</string>
-    <string name="work_folder_name" msgid="3753320833950115786">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎Work‎‏‎‎‏‎"</string>
-    <string name="activity_not_found" msgid="8071924732094499514">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‏‏‏‎‏‎‎App isn\'t installed.‎‏‎‎‏‎"</string>
-    <string name="activity_not_available" msgid="7456344436509528827">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‏‏‏‏‎‏‏‎App isn\'t available‎‏‎‎‏‎"</string>
-    <string name="safemode_shortcut_error" msgid="9160126848219158407">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‏‎‏‏‎‎‎‎‎‎‏‎‏‏‎‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‎‎‎‎‏‏‏‎Downloaded app disabled in Safe mode‎‏‎‎‏‎"</string>
-    <string name="safemode_widget_error" msgid="4863470563535682004">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‎‏‏‎‏‎‏‏‎‏‏‎‎‏‏‎‎‎‏‏‏‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‎‎Widgets disabled in Safe mode‎‏‎‎‏‎"</string>
-    <string name="shortcut_not_available" msgid="2536503539825726397">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‏‎‎‏‏‎‏‏‏‏‎‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‎‏‏‏‏‎‏‎Shortcut isn\'t available‎‏‎‎‏‎"</string>
-    <string name="home_screen" msgid="806512411299847073">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‏‏‎‎‏‏‎‎‎‏‎‏‎‎‏‏‏‎‏‏‎‎‎‎‏‏‏‏‎‏‎‏‏‎‎‏‏‏‏‎‎‎‏‎‎‏‏‎‏‏‏‎‏‎‎‎‎‏‎Home screen‎‏‎‎‏‎"</string>
-    <string name="custom_actions" msgid="3747508247759093328">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‏‎‏‎‎‏‎‏‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‏‎‎‎‎‏‏‎‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎Custom actions‎‏‎‎‏‎"</string>
-    <string name="long_press_widget_to_add" msgid="7699152356777458215">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‎‎‎‏‏‏‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‎‏‎‎‎‏‎‏‎‏‎‏‎‎‏‏‏‏‏‎‏‎‎‎‏‎‎‏‏‏‎Touch &amp; hold to pick up a widget.‎‏‎‎‏‎"</string>
-    <string name="long_accessible_way_to_add" msgid="4289502106628154155">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‏‏‎‎‎‎‏‏‏‎‏‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‎‎‎‎‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‎‎‏‎‏‎‏‏‎Double-tap &amp; hold to pick up a widget or use custom actions.‎‏‎‎‏‎"</string>
-    <string name="widget_dims_format" msgid="2370757736025621599">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‏‏‏‏‏‎%1$d × %2$d‎‏‎‎‏‎"</string>
-    <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‎%1$d wide by %2$d high‎‏‎‎‏‎"</string>
-    <string name="add_item_request_drag_hint" msgid="5899764264480397019">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‏‎‎‎‏‎‏‎‏‏‏‎‎‎‏‏‎‏‎‏‏‏‎‎‏‎‏‏‏‎‏‎‏‏‎‏‏‎‏‏‎‏‏‎Touch &amp; hold to place manually‎‏‎‎‏‎"</string>
-    <string name="place_automatically" msgid="8064208734425456485">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‎‏‏‏‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‎‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‎Add automatically‎‏‎‎‏‎"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‏‎‎‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎Search apps‎‏‎‎‏‎"</string>
-    <string name="all_apps_loading_message" msgid="5813968043155271636">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‏‏‎‏‎‏‏‏‎‏‎‎‏‎‎‎‎‏‏‎‏‎‏‎‏‏‏‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎Loading apps…‎‏‎‎‏‎"</string>
-    <string name="all_apps_no_search_results" msgid="3200346862396363786">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎‏‏‏‏‎‏‎‏‎‏‎‎‎‎‎‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‏‏‏‏‎‏‎‏‎‎‎‎‎‎‏‎‏‎‎No apps found matching \"‎‏‎‎‏‏‎<xliff:g id="QUERY">%1$s</xliff:g>‎‏‎‎‏‏‏‎\"‎‏‎‎‏‎"</string>
-    <string name="all_apps_search_market_message" msgid="1366263386197059176">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‏‏‏‎‎‏‏‎‏‏‏‏‏‎‎‏‎‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎Search for more apps‎‏‎‎‏‎"</string>
-    <string name="label_application" msgid="8531721983832654978">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‏‎‏‏‎‎‎‎‏‏‏‏‏‏‎‎‎‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‎App‎‏‎‎‏‎"</string>
-    <string name="notifications_header" msgid="1404149926117359025">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‎‏‎‏‏‎‎‎‏‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‏‏‎‏‏‎‏‏‎‎‎‏‎Notifications‎‏‎‎‏‎"</string>
-    <string name="long_press_shortcut_to_add" msgid="4524750017792716791">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‎‏‎‎‏‎‎‎‎‏‎‏‎‏‏‎‎‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‏‏‎Touch &amp; hold to pick up a shortcut.‎‏‎‎‏‎"</string>
-    <string name="long_accessible_way_to_add_shortcut" msgid="3327314059613154633">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‎‏‎‎‎‏‏‎‏‏‎‎‏‏‏‏‎‏‎‏‏‎‏‎‏‎‎‏‎‎‏‎Double-tap &amp; hold to pick up a shortcut or use custom actions.‎‏‎‎‏‎"</string>
-    <string name="out_of_space" msgid="4691004494942118364">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‎‎‏‏‎‎‏‏‏‎‎‏‎‏‏‏‎‏‏‏‎‏‏‏‏‏‏‎‎‎‎‏‏‎‎‏‎‎‏‎‎‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎No more room on this Home screen.‎‏‎‎‏‎"</string>
-    <string name="hotseat_out_of_space" msgid="7448809638125333693">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎No more room in the Favorites tray‎‏‎‎‏‎"</string>
-    <string name="all_apps_button_label" msgid="8130441508702294465">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‎‎‏‎‎‎‏‏‏‎‎‎‎‎‏‎Apps list‎‏‎‎‏‎"</string>
-    <string name="all_apps_button_personal_label" msgid="1315764287305224468">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‎‎‎‏‎‏‎‎‎‏‎‎‎‏‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‎‎‎‏‎‎‎‏‎‏‎‎‎Personal apps list‎‏‎‎‏‎"</string>
-    <string name="all_apps_button_work_label" msgid="7270707118948892488">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‏‏‎‏‎‏‏‏‏‎‏‏‎‎‏‏‎‏‎‎‏‎‏‎‎‏‎‏‏‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‎Work apps list‎‏‎‎‏‎"</string>
-    <string name="all_apps_home_button_label" msgid="252062713717058851">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‏‏‎Home‎‏‎‎‏‎"</string>
-    <string name="remove_drop_target_label" msgid="7812859488053230776">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‎‎‎‏‎‏‏‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‎‎‎Remove‎‏‎‎‏‎"</string>
-    <string name="uninstall_drop_target_label" msgid="4722034217958379417">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‏‎‎‎‎‎‎‎‏‎‎‏‎‎‎‏‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‎‎‏‏‎‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‏‎Uninstall‎‏‎‎‏‎"</string>
-    <string name="app_info_drop_target_label" msgid="692894985365717661">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‏‎‎‏‏‏‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‎‎‏‎‏‏‎‏‏‎‏‎‏‎‏‏‏‏‏‏‎‎‎‎‎‏‎‏‎‎‏‏‏‎‏‎App info‎‏‎‎‏‎"</string>
-    <string name="install_drop_target_label" msgid="2539096853673231757">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‎‏‎Install‎‏‎‎‏‎"</string>
-    <string name="permlab_install_shortcut" msgid="5632423390354674437">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‎‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎install shortcuts‎‏‎‎‏‎"</string>
-    <string name="permdesc_install_shortcut" msgid="923466509822011139">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‏‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‏‏‎‎‎‎‎‎‏‏‎Allows an app to add shortcuts without user intervention.‎‏‎‎‏‎"</string>
-    <string name="permlab_read_settings" msgid="1941457408239617576">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‎‏‎‎‏‏‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‎‏‏‎‎‎‏‎‎‎‏‏‎‎‎‏‎‏‎‎‎‎read Home settings and shortcuts‎‏‎‎‏‎"</string>
-    <string name="permdesc_read_settings" msgid="5833423719057558387">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‏‎‎‎‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‏‎‎‏‏‎Allows the app to read the settings and shortcuts in Home.‎‏‎‎‏‎"</string>
-    <string name="permlab_write_settings" msgid="3574213698004620587">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‏‏‏‎‏‏‏‎‎‏‏‏‎‎‏‏‎‏‎‎‏‎‏‎‏‎‎‏‎‏‎‏‏‎write Home settings and shortcuts‎‏‎‎‏‎"</string>
-    <string name="permdesc_write_settings" msgid="5440712911516509985">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‏‎‎‏‎‏‏‏‎‎‏‎‎‎‏‎‎‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‎‏‏‏‎‎‏‎‎‎‎‏‎Allows the app to change the settings and shortcuts in Home.‎‏‎‎‏‎"</string>
-    <string name="msg_no_phone_permission" msgid="9208659281529857371">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‏‎‏‎‏‏‎‏‎‎‏‏‏‎‏‏‎‏‎‏‎‎‏‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‏‏‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ is not allowed to make phone calls‎‏‎‎‏‎"</string>
-    <string name="gadget_error_text" msgid="6081085226050792095">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‏‏‎‎‏‎‎‎‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‎‏‎‎‏‎‏‏‏‎‏‎‎‎‏‎‎‎‏‎‏‎‏‎‏‎‎‏‏‏‏‏‎Problem loading widget‎‏‎‎‏‎"</string>
-    <string name="gadget_setup_text" msgid="8274003207686040488">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‎‎‏‏‎‎‏‎‏‎‏‎‎‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‏‎‎‏‎‎‎‏‏‎‏‎‏‎‏‏‏‎‏‎‏‎‎‎‎Setup‎‏‎‎‏‎"</string>
-    <string name="uninstall_system_app_text" msgid="4172046090762920660">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‎‏‏‎‎‎‎‏‎‏‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‎‏‎‎‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‎‏‎‏‎‎‎This is a system app and can\'t be uninstalled.‎‏‎‎‏‎"</string>
-    <string name="folder_hint_text" msgid="6617836969016293992">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‏‎‏‏‏‎‏‎‎‎‏‏‏‏‎‏‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‎‏‏‎‏‎‎‎‎Unnamed Folder‎‏‎‎‏‎"</string>
-    <string name="disabled_app_label" msgid="6673129024321402780">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‎‏‏‎‏‏‏‎‏‏‎‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‎‎‎‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‎‏‏‏‎‎‎Disabled ‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <plurals name="dotted_app_label" formatted="false" msgid="5194538107138265416">
-      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‏‏‎‏‎‏‏‎‏‎‎‏‏‏‎‎‎‏‎‏‏‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME_2">%1$s</xliff:g>‎‏‎‎‏‏‏‎, has ‎‏‎‎‏‏‎<xliff:g id="NOTIFICATION_COUNT_3">%2$d</xliff:g>‎‏‎‎‏‏‏‎ notifications‎‏‎‎‏‎</item>
-      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‏‏‎‏‎‏‏‎‏‎‎‏‏‏‎‎‎‏‎‏‏‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME_0">%1$s</xliff:g>‎‏‎‎‏‏‏‎, has ‎‏‎‎‏‏‎<xliff:g id="NOTIFICATION_COUNT_1">%2$d</xliff:g>‎‏‎‎‏‏‏‎ notification‎‏‎‎‏‎</item>
-    </plurals>
-    <string name="default_scroll_format" msgid="7475544710230993317">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‏‏‏‏‎‎‏‏‎‏‎‎‏‏‎‏‎‎‏‎‏‎Page %1$d of %2$d‎‏‎‎‏‎"</string>
-    <string name="workspace_scroll_format" msgid="8458889198184077399">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‏‎‎‎‎‎‎‎‎‏‎‏‏‏‎‏‎‎‎‏‎‎‏‏‏‎‎‎‎‏‎‎‏‏‎‏‎‎‏‏‎‎‎‎‏‎‏‎‏‏‏‎Home screen %1$d of %2$d‎‏‎‎‏‎"</string>
-    <string name="workspace_new_page" msgid="257366611030256142">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‏‎‎‏‎‎‏‎‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‏‏‎‏‏‎‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‏‎‏‎‎‎‎‎‏‏‏‎‎New home screen page‎‏‎‎‏‎"</string>
-    <string name="folder_opened" msgid="94695026776264709">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎‎‎‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‎‏‎‏‎Folder opened, ‎‏‎‎‏‏‎<xliff:g id="WIDTH">%1$d</xliff:g>‎‏‎‎‏‏‏‎ by ‎‏‎‎‏‏‎<xliff:g id="HEIGHT">%2$d</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="folder_tap_to_close" msgid="4625795376335528256">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‎‎‎‎‎‎‏‏‎‎‎‏‎‏‏‏‎‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‏‎‏‎‎‎‎‎‎‎Tap to close folder‎‏‎‎‏‎"</string>
-    <string name="folder_tap_to_rename" msgid="4017685068016979677">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‎‏‎‎‏‎‎‎‎‏‏‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‎‏‏‏‎‏‏‎‏‏‏‎‏‎Tap to save rename‎‏‎‎‏‎"</string>
-    <string name="folder_closed" msgid="4100806530910930934">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‏‏‎‏‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‏‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‏‏‏‏‎‏‏‎‎Folder closed‎‏‎‎‏‎"</string>
-    <string name="folder_renamed" msgid="1794088362165669656">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‏‏‎‎‏‎‏‏‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‏‎‎‎‏‏‎‎‎‎Folder renamed to ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="folder_name_format" msgid="6629239338071103179">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‏‏‎‎‏‎‏‏‎Folder: ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="widget_button_text" msgid="2880537293434387943">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‎‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‎‏‎‏‏‎‏‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‏‏‏‎Widgets‎‏‎‎‏‎"</string>
-    <string name="wallpaper_button_text" msgid="8404103075899945851">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‏‏‎‎‏‏‎‏‏‏‏‎‏‏‎Wallpapers‎‏‎‎‏‎"</string>
-    <string name="styles_wallpaper_button_text" msgid="4342122323125579619">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‏‎‎‎‎‏‎‎‏‎‏‎‎‎‏‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‏‎‎‏‏‎‎‎‏‏‎‏‎‏‏‏‏‎‏‏‎‎‎‏‏‎Styles &amp; wallpapers‎‏‎‎‏‎"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‎‏‏‎‎‏‏‏‎‎‏‎‎‎‎‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‏‏‎‎‎‎Home settings‎‏‎‎‏‎"</string>
-    <string name="msg_disabled_by_admin" msgid="6898038085516271325">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‎‎‎‎‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‏‎‎‏‎‏‎‎‎‎‎‎‎‏‏‏‏‎‏‏‎‏‏‏‎‏‎Disabled by your admin‎‏‎‎‏‎"</string>
-    <string name="allow_rotation_title" msgid="7728578836261442095">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‏‎‏‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‏‏‎‎‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎Allow Home screen rotation‎‏‎‎‏‎"</string>
-    <string name="allow_rotation_desc" msgid="8662546029078692509">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‏‏‎‏‏‏‏‎‏‎‎‎‎‏‏‎‏‏‎‏‎‎‏‏‎‎‎‎‏‎‏‎‎‏‏‏‎‏‎When phone is rotated‎‏‎‎‏‎"</string>
-    <string name="notification_dots_title" msgid="9062440428204120317">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‏‎‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‏‎Notification dots‎‏‎‎‏‎"</string>
-    <string name="notification_dots_desc_on" msgid="1679848116452218908">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‎‏‏‎‎‎‏‏‎‎‎‏‏‎‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‎‎‏‏‏‎‎‎On‎‏‎‎‏‎"</string>
-    <string name="notification_dots_desc_off" msgid="1760796511504341095">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‎‏‏‎‏‏‎‎‎‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‎‎‏‏‏‎Off‎‏‎‎‏‎"</string>
-    <string name="title_missing_notification_access" msgid="7503287056163941064">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‏‏‏‎‎‏‏‏‎‎‎‎‎‎‏‎‏‎‏‎‏‏‎‎‏‎‎‎‎Notification access needed‎‏‎‎‏‎"</string>
-    <string name="msg_missing_notification_access" msgid="281113995110910548">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‎‏‎‎‏‎‏‎‏‎‎‎To show Notification Dots, turn on app notifications for ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="title_change_settings" msgid="1376365968844349552">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‏‏‎‎‏‏‎‏‏‎‏‏‏‏‎‎‏‏‏‎‏‏‎‎‎‎‎‎‏‏‏‎‎‎‎‎Change settings‎‏‎‎‏‎"</string>
-    <string name="notification_dots_service_title" msgid="4284221181793592871">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‏‏‏‎Show notification dots‎‏‎‎‏‎"</string>
-    <string name="auto_add_shortcuts_label" msgid="8222286205987725611">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‏‏‏‏‏‏‎‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‏‎Add icon to Home screen‎‏‎‎‏‎"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‎‏‏‏‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‏‎‎‎‎‎‏‎‏‏‏‏‏‎‏‎‏‎‏‎‎For new apps‎‏‎‎‏‎"</string>
-    <string name="package_state_unknown" msgid="7592128424511031410">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‏‏‏‎‎‏‎‏‎‏‎‎‎‏‎‎‏‎‎‏‏‏‏‎‎‏‎‎‎‎‎‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‎‎‏‎‎Unknown‎‏‎‎‏‎"</string>
-    <string name="abandoned_clean_this" msgid="7610119707847920412">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‎‏‏‏‎‏‏‏‎‎‎‏‏‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‎Remove‎‏‎‎‏‎"</string>
-    <string name="abandoned_search" msgid="891119232568284442">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‎‎‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‎‏‎‎‎‏‏‎‏‎‎Search‎‏‎‎‏‎"</string>
-    <string name="abandoned_promises_title" msgid="7096178467971716750">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‏‏‏‏‎‏‎‏‎‏‏‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‏‎‎‎‏‏‏‎‏‎‏‏‎‏‎‏‏‎‏‎‎‎‏‏‏‎‎This app is not installed‎‏‎‎‏‎"</string>
-    <string name="abandoned_promise_explanation" msgid="3990027586878167529">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‎‏‏‎‏‏‎‎‏‏‏‏‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‏‏‏‏‎‏‎‎‏‎The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually.‎‏‎‎‏‎"</string>
-    <string name="app_downloading_title" msgid="8336702962104482644">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‎‎‏‏‏‏‎‏‎‏‏‎‎‏‎‏‏‎‎‏‎‎‏‏‏‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ downloading, ‎‏‎‎‏‏‎<xliff:g id="PROGRESS">%2$s</xliff:g>‎‏‎‎‏‏‏‎ complete‎‏‎‎‏‎"</string>
-    <string name="app_waiting_download_title" msgid="7053938513995617849">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‎‏‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‏‎‎‎‏‎‎‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ waiting to install‎‏‎‎‏‎"</string>
-    <string name="widgets_bottom_sheet_title" msgid="2904559530954183366">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‎‎‎‏‎‎‎‏‏‎‎‎‎‎‎‏‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ widgets‎‏‎‎‏‎"</string>
-    <string name="widgets_list" msgid="796804551140113767">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‏‎‏‏‎‏‎‎‎‏‏‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‏‎‎‏‏‏‎Widgets list‎‏‎‎‏‎"</string>
-    <string name="widgets_list_closed" msgid="6141506579418771922">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‎‎‏‏‏‎‏‏‎‎‎‎‎‎‏‏‏‎‏‏‎‎‎‏‏‏‏‎‎‎‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‎‎‏‏‏‎‏‎‎‏‎‎Widgets list closed‎‏‎‎‏‎"</string>
-    <string name="action_add_to_workspace" msgid="8902165848117513641">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‎‏‏‎‏‏‎‎‎‏‎‏‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‏‎‏‎‎‎‏‎‎‎‏‏‎‏‏‎‏‎‏‎‎‏‎Add to Home screen‎‏‎‎‏‎"</string>
-    <string name="action_move_here" msgid="2170188780612570250">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‏‏‎‎‏‎‏‎‏‎‎‎‏‏‏‏‏‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‎‎‏‎‏‎‎Move item here‎‏‎‎‏‎"</string>
-    <string name="item_added_to_workspace" msgid="4211073925752213539">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‎‏‏‎‏‏‎‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎Item added to home screen‎‏‎‎‏‎"</string>
-    <string name="item_removed" msgid="851119963877842327">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‎‎‎‎‏‎‏‏‏‎‏‎‎‏‏‏‎‏‏‎‎‏‎‏‏‏‎Item removed‎‏‎‎‏‎"</string>
-    <string name="undo" msgid="4151576204245173321">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‏‎‎‏‎‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‎‎‏‎‎‏‎Undo‎‏‎‎‏‎"</string>
-    <string name="action_move" msgid="4339390619886385032">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‎‏‎‎‏‏‏‎‎‏‏‎‎‎‏‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‏‏‏‎‎‎‏‎‎‎‎Move item‎‏‎‎‏‎"</string>
-    <string name="move_to_empty_cell" msgid="2833711483015685619">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‎‎‏‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‎‏‎‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‏‏‎‎‏‎‏‏‏‏‏‎‎‏‏‎Move to row ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%1$s</xliff:g>‎‏‎‎‏‏‏‎ column ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="move_to_position" msgid="6750008980455459790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‏‏‎‎‏‏‎‏‏‎‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‎‎Move to position ‎‏‎‎‏‏‎<xliff:g id="NUMBER">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="move_to_hotseat_position" msgid="6295412897075147808">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‎‏‏‏‎‏‏‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‎‎‏‎‎‏‎‎‏‎‎‏‏‏‎‏‏‎‏‎‎‎‎‏‎‎‎‎‎‎Move to favorites position ‎‏‎‎‏‏‎<xliff:g id="NUMBER">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="item_moved" msgid="4606538322571412879">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‏‏‎Item moved‎‏‎‎‏‎"</string>
-    <string name="add_to_folder" msgid="9040534766770853243">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‏‏‏‏‎‏‏‎Add to folder: ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="add_to_folder_with_app" msgid="4534929978967147231">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‏‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‎‎‎‏‎‏‏‎‏‏‏‏‏‎Add to folder with ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="added_to_folder" msgid="4793259502305558003">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‎‏‎‎‎‏‎‏‎‎‎‎‎‏‏‏‏‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎Item added to folder‎‏‎‎‏‎"</string>
-    <string name="create_folder_with" msgid="4050141361160214248">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‏‎‎‏‎‎‏‎‏‎‏‏‎‏‏‏‎‏‎‎‎‎Create folder with: ‎‏‎‎‏‏‎<xliff:g id="NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="folder_created" msgid="6409794597405184510">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‏‎‏‎‎‎‎‏‎‏‎‏‎‎‎‏‏‏‎‎‏‏‏‎‎‏‏‎‏‏‎‎‎‏‏‎‏‏‏‎‏‎‎‎‏‏‏‏‏‏‏‏‎‎Folder created‎‏‎‎‏‎"</string>
-    <string name="action_move_to_workspace" msgid="1603837886334246317">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‎‏‎‎‎‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‏‎‏‎‏‏‎‏‎Move to Home screen‎‏‎‎‏‎"</string>
-    <string name="action_resize" msgid="1802976324781771067">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‏‎‎‎‎‎‏‎‏‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‎‎‏‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‏‎‎‏‎‎‏‏‏‎‏‏‎Resize‎‏‎‎‏‎"</string>
-    <string name="action_increase_width" msgid="8773715375078513326">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‏‎‏‏‏‎‎Increase width‎‏‎‎‏‎"</string>
-    <string name="action_increase_height" msgid="459390020612501122">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‏‎‎‏‏‎‎‏‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‎‏‎‎‎‎‎‏‎‎Increase height‎‏‎‎‏‎"</string>
-    <string name="action_decrease_width" msgid="1374549771083094654">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‏‏‎‏‏‎‎‎‎‏‏‎‏‏‏‎‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‎‎‎‏‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎Decrease width‎‏‎‎‏‎"</string>
-    <string name="action_decrease_height" msgid="282377193880900022">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‏‎‏‎‎‏‎‎‎‏‏‏‏‏‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‏‎‎‎‏‏‎‏‏‎‏‏‎‎Decrease height‎‏‎‎‏‎"</string>
-    <string name="widget_resized" msgid="9130327887929620">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‎‎‎Widget resized to width ‎‏‎‎‏‏‎<xliff:g id="NUMBER_0">%1$s</xliff:g>‎‏‎‎‏‏‏‎ height ‎‏‎‎‏‏‎<xliff:g id="NUMBER_1">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-    <string name="action_deep_shortcut" msgid="2864038805849372848">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‎‏‎‏‎‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‎‎‎Shortcuts‎‏‎‎‏‎"</string>
-    <string name="shortcuts_menu_with_notifications_description" msgid="2676582286544232849">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‎‎‎‏‎‏‏‎‏‎‎‎‏‎‎‏‎‎‎‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‏‏‎‎‏‎‎‎‏‎Shortcuts and notifications‎‏‎‎‏‎"</string>
-    <string name="action_dismiss_notification" msgid="5909461085055959187">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‏‏‎Dismiss‎‏‎‎‏‎"</string>
-    <string name="notification_dismissed" msgid="6002233469409822874">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‎‏‏‏‏‎‎‎‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‎Notification dismissed‎‏‎‎‏‎"</string>
-    <string name="all_apps_personal_tab" msgid="4190252696685155002">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‏‎‎‏‏‎‏‏‎‎‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‏‎‎‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‏‏‏‎‏‎‎Personal‎‏‎‎‏‎"</string>
-    <string name="all_apps_work_tab" msgid="4884822796154055118">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‏‎‎‏‎‏‎‎‏‏‎‎‎‎‎‎‏‏‏‎‎‏‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‏‎‎‏‏‏‎‎Work‎‏‎‎‏‎"</string>
-    <string name="work_profile_toggle_label" msgid="3081029915775481146">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‏‏‎‎‎‏‏‎‏‎‏‎‎‏‏‏‎‏‎‎Work profile‎‏‎‎‏‎"</string>
-    <string name="bottom_work_tab_user_education_title" msgid="5785851780786322825">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‎‏‏‏‏‎‎‏‏‎‎‎‏‎‏‎‎‎‏‏‎‏‎‎‏‏‎‏‎‏‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎Find work apps here‎‏‎‎‏‎"</string>
-    <string name="bottom_work_tab_user_education_body" msgid="2818107472360579152">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‏‏‎‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‏‎‏‎‎‎‎‎Each work app has a badge and is kept secure by your organization. Move apps to your Home screen for easier access.‎‏‎‎‏‎"</string>
-    <string name="work_mode_on_label" msgid="4781128097185272916">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‎‎‎‏‎‎‎‎‏‎‎‏‎‏‏‏‏‎‎‎‎‏‎‏‎‎‎‏‎‏‎‏‎‎‎Managed by your organization‎‏‎‎‏‎"</string>
-    <string name="work_mode_off_label" msgid="3194894777601421047">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‏‎‏‏‎‏‎‎‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‏‏‎‏‎‏‏‏‎‏‏‏‎‏‏‏‎‏‏‏‏‎‏‏‏‎Notifications and apps are off‎‏‎‎‏‎"</string>
-    <string name="bottom_work_tab_user_education_close_button" msgid="4224492243977802135">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‎‎‎‎‏‏‎‏‎‎‏‎‏‎‏‎‎‎‎‎‏‎‎‎‏‏‎‏‎‏‏‎‎‎‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‏‎Close‎‏‎‎‏‎"</string>
-    <string name="bottom_work_tab_user_education_closed" msgid="1098340939861869465">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‏‏‏‏‎‎‎‎‏‎‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎‎‏‎‎‎‎‏‏‏‎‎‏‏‎‎‏‎Closed‎‏‎‎‏‎"</string>
-    <string name="remote_action_failed" msgid="1383965239183576790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‏‎‏‎‎‏‏‎‏‎‏‎‏‎‎‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‎‎‎‎‏‏‎‏‎‎‎‏‏‏‎‏‏‎‏‎‏‏‎‎Failed: ‎‏‎‎‏‏‎<xliff:g id="WHAT">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
-</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 960ef93..d10c84b 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabajo"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"No se instaló la aplicación."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"La aplicación no está disponible."</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 01f60e4..09b1239 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabajo"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"La aplicación no está instalada."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"La aplicación no está disponible"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index ff121fa..1e470e1 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Töö"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Rakendus pole installitud."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Rakendus ei ole saadaval"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 913ff48..926cdb9 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"کاری"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"برنامه نصب نشده است."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"برنامه در دسترس نیست"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index e0930b6..f87441f 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Työ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Sovellusta ei ole asennettu."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Sovellus ei ole käytettävissä"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 50d6a06..5ac514d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Lanceur3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Travail"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"L\'application n\'est pas installée."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Application indisponible"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index da9da38..3228dea 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -80,7 +80,7 @@
     <string name="widget_button_text" msgid="2880537293434387943">"વિજેટ્સ"</string>
     <string name="wallpaper_button_text" msgid="8404103075899945851">"વૉલપેપર્સ"</string>
     <string name="styles_wallpaper_button_text" msgid="4342122323125579619">"શૈલીઓ અને વૉલપેપર"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"હોમ સેટિંગ"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"હોમ સેટિંગ્સ"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"તમારા વ્યવસ્થાપક દ્વારા અક્ષમ કરેલ"</string>
     <string name="allow_rotation_title" msgid="7728578836261442095">"હોમ સ્ક્રીનને ફેરવવાની મંજૂરી આપો"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"જ્યારે ફોન ફેરવવામાં આવે ત્યારે"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 7e26ca4..3327590 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Munka"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Az alkalmazás nincs telepítve."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Az alkalmazás nem érhető el"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 9d26c8c..d66ca32 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Vinna"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Forritið er ekki uppsett."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Forritið er ekki í boði"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index aaceb53..8c4e3c5 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Lavoro"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App non installata."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App non disponibile"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 3525699..c25e879 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"עבודה"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"האפליקציה לא מותקנת."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"האפליקציה אינה זמינה"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index dad4879..354f020 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"仕事用"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"このアプリはインストールされていません。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"このアプリは使用できません"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index b348afb..e9afef9 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"სამუშაო"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"აპი არ არის დაყენებული."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"აპი მიუწვდომელია"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index bd25ac9..1cd9045 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Жұмыс"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Қолданба орнатылмаған."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Қолданба қол жетімді емес"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index e2767f9..58b65da 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"업무"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"앱이 설치되지 않았습니다."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"앱을 사용할 수 없음"</string>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index bc658e4..662b86e 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -15,9 +15,6 @@
 -->
 
 <resources>
-    <!-- Container -->
-    <item name="container_margin" format="fraction" type="fraction">12%</item>
-
     <!-- Fast scroll -->
     <dimen name="fastscroll_popup_width">58dp</dimen>
     <dimen name="fastscroll_popup_height">48dp</dimen>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index e764347..afe7664 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ວຽກ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ແອັບຯບໍ່ໄດ້ຖືກຕິດຕັ້ງ."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ແອັບຯ​ໃຊ້​ບໍ່​ໄດ້"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 7f93ac8..6571582 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Darbas"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Programa neįdiegta."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Programa nepasiekiama"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 13d88fe..3da0fbb 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Darbs"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Lietotne nav instalēta."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Lietotne nav pieejama."</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 458fb73..ab14e89 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -80,9 +80,9 @@
     <string name="widget_button_text" msgid="2880537293434387943">"Виџети"</string>
     <string name="wallpaper_button_text" msgid="8404103075899945851">"Тапети"</string>
     <string name="styles_wallpaper_button_text" msgid="4342122323125579619">"Стилови и тапети"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"Поставки за почетен екран"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"Поставки за Home"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Оневозможено од администраторот"</string>
-    <string name="allow_rotation_title" msgid="7728578836261442095">"Дозволете ротација на почетниот екран"</string>
+    <string name="allow_rotation_title" msgid="7728578836261442095">"Дозволете ротација на Почетниот екран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Кога телефонот се ротира"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"Точки за известување"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Вклучено"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index a118368..4362e7c 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"ലോഞ്ചർ3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ഔദ്യോഗികം"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"അപ്ലിക്കേഷൻ ഇൻസ്‌റ്റാളുചെ‌യ്‌തിട്ടില്ല."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"അപ്ലിക്കേഷൻ ലഭ്യമല്ല"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 1b0e753..ab02ac6 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Ажил"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Апп суугаагүй байна."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Апп-г ашиглах боломжгүй"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 69eb5ee..49e3898 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"कार्य"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"अ‍ॅप इंस्टॉल केलेला नाही."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"अ‍ॅप उपलब्ध नाही"</string>
@@ -46,7 +47,7 @@
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"आवडीच्या ट्रे मध्ये आणखी जागा नाही"</string>
     <string name="all_apps_button_label" msgid="8130441508702294465">"अ‍ॅप्स सूची"</string>
     <string name="all_apps_button_personal_label" msgid="1315764287305224468">"वैयक्तिक अ‍ॅप्स सूची"</string>
-    <string name="all_apps_button_work_label" msgid="7270707118948892488">"कामाच्या ठिकाणी वापरली जाणाऱ्या ॲप्सची सूची"</string>
+    <string name="all_apps_button_work_label" msgid="7270707118948892488">"कामाच्या ठिकाणी वापरली जाणाऱ्या अॅप्सची सूची"</string>
     <string name="all_apps_home_button_label" msgid="252062713717058851">"होम"</string>
     <string name="remove_drop_target_label" msgid="7812859488053230776">"काढा"</string>
     <string name="uninstall_drop_target_label" msgid="4722034217958379417">"अनइंस्टॉल करा"</string>
@@ -82,7 +83,7 @@
     <string name="styles_wallpaper_button_text" msgid="4342122323125579619">"शैली आणि वॉलपेपर"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"होम सेटिंग्‍ज"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"आपल्या प्रशासकाने अक्षम केले"</string>
-    <string name="allow_rotation_title" msgid="7728578836261442095">"मुख्य स्क्रीन फिरविण्‍यास अनुमती द्या"</string>
+    <string name="allow_rotation_title" msgid="7728578836261442095">"मुख्यस्क्रीन फिरविण्‍यास अनुमती द्या"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फोन फिरविला जातो तेव्हा"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"सूचना बिंदू"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"सुरू"</string>
@@ -92,7 +93,7 @@
     <string name="title_change_settings" msgid="1376365968844349552">"सेटिंग्ज बदला"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"सूचना बिंदू दाखवा"</string>
     <string name="auto_add_shortcuts_label" msgid="8222286205987725611">"होम स्क्रीनवर आयकन जोडा"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"नवीन ॲप्ससाठी"</string>
+    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"नवीन अॅप्ससाठी"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"अज्ञात"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"काढा"</string>
     <string name="abandoned_search" msgid="891119232568284442">"शोधा"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 492e6b6..7d05412 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Kerja"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Apl tidak dipasang."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Apl tidak tersedia"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 356ed03..2257367 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Jobb"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen er ikke installert."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen er ikke tilgjengelig"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 58d61f9..1e7aee9 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"कार्य"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"अनुप्रयोग स्थापित छैन।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"अनुप्रयोग उपलब्ध छैन"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index c2dfb71..ec30d8c 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Werk"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App is niet geïnstalleerd."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App is niet beschikbaar"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 2479d1b..4ddc903 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"ଲଞ୍ଚର୍3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"କାମ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ଆପ୍‌ ଇନଷ୍ଟଲ୍‌ ହୋଇନାହିଁ"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ଆପ୍‌ ଉପଲବ୍ଧ ନାହିଁ"</string>
@@ -84,8 +85,8 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଅକ୍ଷମ କରାଯାଇଛି"</string>
     <string name="allow_rotation_title" msgid="7728578836261442095">"ହୋମ୍‌ ସ୍କ୍ରୀନ୍ ବୁଲାଇବା ଅନୁମତି ଦିଅନ୍ତୁ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ବୁଲାଯାଇଥାଏ"</string>
-    <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ଡଟ୍ସ"</string>
-    <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ"</string>
+    <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ବିନ୍ଦୁଗୁଡ଼ିକ"</string>
+    <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ କରନ୍ତୁ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="title_missing_notification_access" msgid="7503287056163941064">"ବିଜ୍ଞପ୍ତି ଆକ୍ସେସ୍‌ ଆବଶ୍ୟକ ଅଟେ"</string>
     <string name="msg_missing_notification_access" msgid="281113995110910548">"ବିଜ୍ଞପ୍ତି ବିନ୍ଦୁ ଦେଖାଇବାକୁ, <xliff:g id="NAME">%1$s</xliff:g> ପାଇଁ ଆପ୍‌ ବିଜ୍ଞପ୍ତି ଅନ୍‌ କରନ୍ତୁ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 24686c4..d4cd5be 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ਦਫ਼ਤਰ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ਐਪ ਇੰਸਟੌਲ ਨਹੀਂ ਕੀਤਾ ਹੋਇਆ ਹੈ।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 48c8baf..6885960 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Praca"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacja nie jest zainstalowana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacja niedostępna"</string>
@@ -129,7 +130,7 @@
     <string name="widget_resized" msgid="9130327887929620">"Szerokość i wysokość widżetu zmieniła się na <xliff:g id="NUMBER_0">%1$s</xliff:g> x <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
     <string name="action_deep_shortcut" msgid="2864038805849372848">"Skróty"</string>
     <string name="shortcuts_menu_with_notifications_description" msgid="2676582286544232849">"Skróty i powiadomienia"</string>
-    <string name="action_dismiss_notification" msgid="5909461085055959187">"Zamknij"</string>
+    <string name="action_dismiss_notification" msgid="5909461085055959187">"Odrzuć"</string>
     <string name="notification_dismissed" msgid="6002233469409822874">"Powiadomienie odrzucone"</string>
     <string name="all_apps_personal_tab" msgid="4190252696685155002">"Osobiste"</string>
     <string name="all_apps_work_tab" msgid="4884822796154055118">"Praca"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 1cc699f..23b00d0 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Работа"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Приложение удалено"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Приложение недоступно"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 2f9dc42..ef99a59 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"කාර්යාලය"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"යෙදුම ස්ථාපනය කර නැත."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"යෙදුම නොතිබේ"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 417151b..044a4b4 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Služba"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija ni nameščena."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija ni na voljo"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 5e2c954..4e52592 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Апликација није инсталирана."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Апликација није доступна"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 7df507b..e7400a6 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbete"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen är inte installerad."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen är inte tillgänglig"</string>
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
index eb9af97..09bdaaf 100644
--- a/res/values-sw600dp/config.xml
+++ b/res/values-sw600dp/config.xml
@@ -1,4 +1,3 @@
 <resources>
-    <bool name="is_tablet">true</bool>
     <bool name="allow_rotation">true</bool>
 </resources>
diff --git a/res/values-sw720dp/config.xml b/res/values-sw720dp/config.xml
index 94cffcb..1f401c4 100644
--- a/res/values-sw720dp/config.xml
+++ b/res/values-sw720dp/config.xml
@@ -1,6 +1,5 @@
 <resources>
     <bool name="config_largeHeap">true</bool>
-    <bool name="is_large_tablet">true</bool>
 
 <!-- All Apps & Widgets -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 799b6b8..6e933a1 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -36,7 +36,7 @@
     <string name="place_automatically" msgid="8064208734425456485">"தானாகவே சேர்"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"பயன்பாடுகளில் தேடுக"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"ஆப்ஸை ஏற்றுகிறது…"</string>
-    <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" உடன் பொருந்தும் ஆப்ஸ் இல்லை"</string>
+    <string name="all_apps_no_search_results" msgid="3200346862396363786">"\"<xliff:g id="QUERY">%1$s</xliff:g>\" உடன் பொருந்தும் பயன்பாடுகள் இல்லை"</string>
     <string name="all_apps_search_market_message" msgid="1366263386197059176">"கூடுதல் பயன்பாடுகளைத் தேடு"</string>
     <string name="label_application" msgid="8531721983832654978">"ஆப்ஸ்"</string>
     <string name="notifications_header" msgid="1404149926117359025">"அறிவிப்புகள்"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index de4863a..0df94c7 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabaho"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Hindi naka-install ang app."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Hindi available ang app"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 1b2b3d1..56f8447 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"İş"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Uygulama yüklü değil."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Uygulama kullanılamıyor"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 622cdcc..13ba701 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Робоча папка"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Додаток видалено."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Додаток недоступний"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 74b5eaa..4f77670 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"دفتری"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ایپ انسٹال نہیں ہے۔"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ایپ دستیاب نہیں ہے"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index dac1ac9..69084d7 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Ishga oid"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Ilova o‘rnatilmadi."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Ilova mavjud emas"</string>
diff --git a/res/values-v26/styles.xml b/res/values-v26/styles.xml
index 8fb408b..d2f0802 100644
--- a/res/values-v26/styles.xml
+++ b/res/values-v26/styles.xml
@@ -20,6 +20,7 @@
     <!-- Theme for the widget container. -->
     <style name="WidgetContainerTheme" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:colorPrimaryDark">#E8EAED</item>
+        <item name="android:textColorSecondary">?android:attr/textColorPrimary</item>
         <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
     </style>
     <style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
diff --git a/SecondaryDisplayLauncher/res/values/styles.xml b/res/values-v30/styles.xml
similarity index 61%
rename from SecondaryDisplayLauncher/res/values/styles.xml
rename to res/values-v30/styles.xml
index 4e41a98..71740a9 100644
--- a/SecondaryDisplayLauncher/res/values/styles.xml
+++ b/res/values-v30/styles.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 /*
-* Copyright (C) 2018 The Android Open Source Project
+* Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -18,13 +18,17 @@
 -->
 
 <resources>
-    <style name="SecondaryLauncherTheme" parent="Theme.MaterialComponents.NoActionBar" >
-        <item name="android:windowShowWallpaper">true</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowIsTranslucent">true</item>
-        <item name="android:windowTranslucentStatus">true</item>
-        <item name="android:windowTranslucentNavigation">true</item>
+    <!-- Launcher theme -->
+    <style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:colorEdgeEffect">#FF757575</item>
+        <item name="android:windowActionBar">false</item>
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowShowWallpaper">true</item>
+        <item name="folderTextColor">?attr/workspaceTextColor</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+        <item name="android:enforceStatusBarContrast">false</item>
+        <item name="android:enforceNavigationBarContrast">false</item>
     </style>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 4dc58b9..71decfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Trình chạy 3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Ứng dụng chưa được cài đặt."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Ứng dụng không có sẵn"</string>
@@ -85,7 +86,7 @@
     <string name="allow_rotation_title" msgid="7728578836261442095">"Cho phép xoay Màn hình chính"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Khi xoay điện thoại"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"Dấu chấm thông báo"</string>
-    <string name="notification_dots_desc_on" msgid="1679848116452218908">"Đang bật"</string>
+    <string name="notification_dots_desc_on" msgid="1679848116452218908">"Bật"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Tắt"</string>
     <string name="title_missing_notification_access" msgid="7503287056163941064">"Cần quyền truy cập thông báo"</string>
     <string name="msg_missing_notification_access" msgid="281113995110910548">"Để hiển thị Dấu chấm thông báo, hãy bật thông báo ứng dụng cho <xliff:g id="NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 0478627..9804af1 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"未安装该应用。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"应用不可用"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index ed53c32..e737744 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"工作"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"尚未安裝應用程式。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"目前無法使用這個應用程式"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 14f2e06..e971b69 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -20,6 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
+    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"公司"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"應用程式未安裝。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"應用程式目前無法使用"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index de17eb7..26e6cba 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -22,6 +22,7 @@
     <attr name="allAppsScrimColor" format="color" />
     <attr name="allAppsInterimScrimAlpha" format="integer" />
     <attr name="allAppsNavBarScrimColor" format="color" />
+    <attr name="allAppsTheme" format="reference" />
     <attr name="popupColorPrimary" format="color" />
     <attr name="popupColorSecondary" format="color" />
     <attr name="popupColorTertiary" format="color" />
@@ -34,12 +35,17 @@
     <attr name="workspaceStatusBarScrim" format="reference" />
     <attr name="widgetsTheme" format="reference" />
     <attr name="loadingIconColor" format="color" />
+    <attr name="iconOnlyShortcutColor" format="color"/>
+    <attr name="eduHalfSheetBGColor" format="color"/>
 
     <attr name="folderDotColor" format="color" />
     <attr name="folderFillColor" format="color" />
     <attr name="folderIconRadius" format="float" />
     <attr name="folderIconBorderColor" format="color" />
     <attr name="folderTextColor" format="color" />
+    <attr name="folderHintColor" format="color" />
+    <attr name="workProfileOverlayTextColor" format="color" />
+    <attr name="disabledIconAlpha" format="float" />
 
     <!-- BubbleTextView specific attributes. -->
     <declare-styleable name="BubbleTextView">
@@ -115,6 +121,9 @@
         <attr name="numFolderColumns" format="integer" />
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
+        <attr name="dbFile" format="string" />
+        <!-- numAllAppsColumns defaults to numColumns, if not specified -->
+        <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
@@ -130,6 +139,12 @@
         <attr name="iconTextSize" format="float" />
         <!-- If true, this display option is used to determine the default grid -->
         <attr name="canBeDefault" format="boolean" />
+
+        <!-- The following values are only enabled if grid is supported. -->
+        <!-- allAppsIconSize defaults to iconSize, if not specified -->
+        <attr name="allAppsIconSize" format="float" />
+        <!-- allAppsIconTextSize defaults to iconTextSize, if not specified -->
+        <attr name="allAppsIconTextSize" format="float" />
     </declare-styleable>
 
     <declare-styleable name="CellLayout">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 3c8fe1e..043ad9a 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -37,4 +37,9 @@
 
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
+
+    <color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
+    <color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
+    <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
+    <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 9d36ee4..ca25325 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,8 +1,6 @@
 <resources>
-<!-- Miscellaneous -->
+    <!-- Miscellaneous -->
     <bool name="config_largeHeap">false</bool>
-    <bool name="is_tablet">false</bool>
-    <bool name="is_large_tablet">false</bool>
     <bool name="allow_rotation">false</bool>
 
     <integer name="extracted_color_gradient_alpha">153</integer>
@@ -21,10 +19,10 @@
     <!-- String representing the fragment class for settings activity.-->
     <string name="settings_fragment_name" translatable="false">com.android.launcher3.settings.SettingsActivity$LauncherSettingsFragment</string>
 
-<!-- DragController -->
+    <!-- DragController -->
     <item type="id" name="drag_event_parity" />
 
-<!-- AllApps & Launcher transitions -->
+    <!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
     <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
 
@@ -34,7 +32,7 @@
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
-<!-- Workspace -->
+    <!-- Workspace -->
     <!-- The duration (in ms) of the fade animation on the object outlines, used when
          we are dragging objects around on the home screen. -->
     <integer name="config_dragOutlineFadeTime">900</integer>
@@ -57,28 +55,21 @@
     <!-- The duration of the caret animation -->
     <integer name="config_caretAnimationDuration">200</integer>
 
-<!-- Hotseat -->
+    <!-- Hotseat -->
     <bool name="hotseat_transpose_layout_with_orientation">true</bool>
 
     <!-- Various classes overriden by projects/build flavors. -->
     <string name="app_filter_class" translatable="false"></string>
-    <string name="icon_provider_class" translatable="false"></string>
-    <string name="drawable_factory_class" translatable="false"></string>
     <string name="user_event_dispatcher_class" translatable="false"></string>
+    <string name="folder_name_provider_class" translatable="false"></string>
     <string name="stats_log_manager_class" translatable="false"></string>
     <string name="app_transition_manager_class" translatable="false"></string>
     <string name="instant_app_resolver_class" translatable="false"></string>
     <string name="main_process_initializer_class" translatable="false"></string>
-    <string name="system_shortcut_factory_class" translatable="false"></string>
     <string name="app_launch_tracker_class" translatable="false"></string>
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="launcher_activity_logic_class" translatable="false"></string>
-
-    <!-- Package name of the default wallpaper picker. -->
-    <string name="wallpaper_picker_package" translatable="false"></string>
-
-    <!-- Whitelisted package to retrieve packagename for badge. Can be empty. -->
-    <string name="shortcutinfo_badgepkg_whitelist" translatable="false"></string>
+    <string name="prediction_model_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
@@ -97,7 +88,12 @@
     <integer name="config_popupArrowOpenCloseDuration">40</integer>
     <integer name="config_removeNotificationViewDuration">300</integer>
 
-<!-- Accessibility actions -->
+    <!-- Default packages -->
+    <string name="wallpaper_picker_package" translatable="false"></string>
+    <string name="calendar_component_name" translatable="false"></string>
+    <string name="clock_component_name" translatable="false"></string>
+
+    <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
     <item type="id" name="action_uninstall" />
     <item type="id" name="action_reconfigure" />
@@ -111,14 +107,84 @@
     <item type="id" name="action_shortcuts_and_notifications"/>
     <item type="id" name="action_dismiss_notification" />
     <item type="id" name="action_remote_action_shortcut" />
+    <item type="id" name="action_dismiss_prediction" />
+    <item type="id" name="action_pin_prediction"/>
 
-<!-- QSB IDs. DO not change -->
+    <!-- QSB IDs. DO not change -->
     <item type="id" name="search_container_workspace" />
     <item type="id" name="search_container_all_apps" />
 
-<!-- Recents -->
+    <!-- Recents -->
     <item type="id" name="overview_panel"/>
 
+    <!-- Whether to enable background preloading of task thumbnails. -->
+    <bool name="config_enableTaskSnapshotPreloading">true</bool>
+
+    <!-- Configuration resources -->
+    <item name="all_apps_spring_damping_ratio" type="dimen" format="float">0.75</item>
+    <item name="all_apps_spring_stiffness" type="dimen" format="float">600</item>
+
+    <item name="dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.73</item>
+    <item name="dismiss_task_trans_y_stiffness" type="dimen" format="float">800</item>
+
+    <item name="dismiss_task_trans_x_damping_ratio" type="dimen" format="float">0.73</item>
+    <item name="dismiss_task_trans_x_stiffness" type="dimen" format="float">800</item>
+
+    <item name="horizontal_spring_damping_ratio" type="dimen" format="float">0.8</item>
+    <item name="horizontal_spring_stiffness" type="dimen" format="float">250</item>
+
+    <item name="swipe_up_rect_scale_damping_ratio" type="dimen" format="float">0.75</item>
+    <item name="swipe_up_rect_scale_stiffness" type="dimen" format="float">200</item>
+
+    <item name="swipe_up_rect_xy_fling_friction" type="dimen" format="float">1.5</item>
+    <item name="swipe_up_rect_xy_damping_ratio" type="dimen" format="float">0.8</item>
+    <item name="swipe_up_rect_xy_stiffness" type="dimen" format="float">200</item>
+
+    <item name="staggered_damping_ratio" type="dimen" format="float">0.7</item>
+    <item name="staggered_stiffness" type="dimen" format="float">150</item>
+    <dimen name="unlock_staggered_velocity_dp_per_s">3dp</dimen>
+
+    <item name="hint_scale_damping_ratio" type="dimen" format="float">0.7</item>
+    <item name="hint_scale_stiffness" type="dimen" format="float">200</item>
+    <dimen name="hint_scale_velocity_dp_per_s">0.3dp</dimen>
+
+    <!-- Swipe up to home related -->
+    <dimen name="swipe_up_fling_min_visible_change">18dp</dimen>
+    <dimen name="swipe_up_y_overshoot">10dp</dimen>
+    <dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
+
+    <array name="dynamic_resources">
+        <item>@dimen/all_apps_spring_damping_ratio</item>
+        <item>@dimen/all_apps_spring_stiffness</item>
+
+        <item>@dimen/dismiss_task_trans_y_damping_ratio</item>
+        <item>@dimen/dismiss_task_trans_y_stiffness</item>
+
+        <item>@dimen/dismiss_task_trans_x_damping_ratio</item>
+        <item>@dimen/dismiss_task_trans_x_stiffness</item>
+
+        <item>@dimen/horizontal_spring_damping_ratio</item>
+        <item>@dimen/horizontal_spring_stiffness</item>
+
+        <item>@dimen/swipe_up_rect_scale_damping_ratio</item>
+        <item>@dimen/swipe_up_rect_scale_stiffness</item>
+
+        <item>@dimen/swipe_up_rect_xy_fling_friction</item>
+        <item>@dimen/swipe_up_rect_xy_damping_ratio</item>
+        <item>@dimen/swipe_up_rect_xy_stiffness</item>
+
+        <item>@dimen/staggered_damping_ratio</item>
+        <item>@dimen/staggered_stiffness</item>
+        <item>@dimen/unlock_staggered_velocity_dp_per_s</item>
+
+        <item>@dimen/swipe_up_fling_min_visible_change</item>
+        <item>@dimen/swipe_up_y_overshoot</item>
+
+        <item>@dimen/hint_scale_damping_ratio</item>
+        <item>@dimen/hint_scale_stiffness</item>
+        <item>@dimen/hint_scale_velocity_dp_per_s</item>
+    </array>
+
     <string-array name="live_wallpapers_remove_sysui_scrims">
     </string-array>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 4bcb8a7..947e635 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,7 +20,6 @@
 
     <!-- Dynamic Grid -->
     <dimen name="dynamic_grid_edge_margin">8dp</dimen>
-    <dimen name="dynamic_grid_page_indicator_line_height">1dp</dimen>
     <dimen name="dynamic_grid_icon_drawable_padding">8dp</dimen>
     <!-- Minimum space between workspace and hotseat in spring loaded mode -->
     <dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
@@ -36,10 +35,18 @@
     <dimen name="dynamic_grid_hotseat_extra_vertical_size">34dp</dimen>
     <dimen name="dynamic_grid_hotseat_side_padding">0dp</dimen>
 
+    <!-- Workspace page indicator -->
+    <dimen name="workspace_page_indicator_height">24dp</dimen>
+    <dimen name="workspace_page_indicator_line_height">1dp</dimen>
+    <dimen name="workspace_page_indicator_overlap_workspace">0dp</dimen>
+
     <!-- Hotseat/all-apps scrim -->
     <dimen name="all_apps_scrim_blur">4dp</dimen>
-    <dimen name="vertical_drag_handle_size">24dp</dimen>
-    <dimen name="vertical_drag_handle_overlap_workspace">0dp</dimen>
+    <dimen name="vertical_drag_handle_width">18dp</dimen>
+    <dimen name="vertical_drag_handle_height">6dp</dimen>
+    <dimen name="vertical_drag_handle_elevation">1dp</dimen>
+    <dimen name="vertical_drag_handle_touch_size">48dp</dimen>
+    <dimen name="vertical_drag_handle_padding_in_vertical_bar_layout">16dp</dimen>
 
 <!-- Drop target bar -->
     <dimen name="dynamic_grid_drop_target_size">48dp</dimen>
@@ -85,6 +92,11 @@
     <dimen name="all_apps_tabs_side_padding">12dp</dimen>
     <dimen name="all_apps_divider_height">1dp</dimen>
 
+    <dimen name="all_apps_tip_bottom_margin">8dp</dimen>
+    <!-- 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>
@@ -92,6 +104,9 @@
 
     <dimen name="all_apps_divider_margin_vertical">8dp</dimen>
 
+    <dimen name="work_profile_footer_padding">20dp</dimen>
+    <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>
@@ -187,7 +202,7 @@
     <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>
-    <dimen name="system_shortcut_header_height">40dp</dimen>
+    <dimen name="system_shortcut_header_height">48dp</dimen>
     <dimen name="system_shortcut_header_icon_touch_size">48dp</dimen>
     <!-- (touch_size - icon_size) / 2 -->
     <dimen name="system_shortcut_header_icon_padding">12dp</dimen>
@@ -235,4 +250,7 @@
 <!-- Theming related -->
     <dimen name="default_dialog_corner_radius">8dp</dimen>
 
+    <!-- Onboarding bottomsheet related -->
+    <dimen name="bottom_sheet_edu_padding">24dp</dimen>
+
 </resources>
diff --git a/res/values/drawables.xml b/res/values/drawables.xml
index 1367174..7d63142 100644
--- a/res/values/drawables.xml
+++ b/res/values/drawables.xml
@@ -17,4 +17,6 @@
     <drawable name="ic_setup_shadow">@drawable/ic_setting</drawable>
     <drawable name="ic_remove_shadow">@drawable/ic_remove_no_shadow</drawable>
     <drawable name="ic_uninstall_shadow">@drawable/ic_uninstall_no_shadow</drawable>
+    <drawable name="ic_block_shadow">@drawable/ic_block_no_shadow</drawable>
+    <drawable name="all_apps_arrow_shadow">@drawable/drag_handle_indicator_no_shadow</drawable>
 </resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d9c2e8..935bb40 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -102,9 +102,11 @@
     <string name="app_info_drop_target_label">App info</string>
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
-
     <!-- Label for install dismiss prediction. -->
-    <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+    <string name="dismiss_prediction_label">Don\'t suggest app</string>
+    <!-- Label for pinning predicted app. -->
+    <string name="pin_prediction">Pin Prediction</string>
+
 
     <!-- Permissions: -->
     <skip />
@@ -140,7 +142,7 @@
     <string name="uninstall_system_app_text">This is a system app and can\'t be uninstalled.</string>
 
     <!-- Default folder title -->
-    <string name="folder_hint_text">Unnamed Folder</string>
+    <string name="folder_hint_text">Edit Name</string>
 
     <!-- Accessibility -->
     <!-- The format string for when an app is temporarily disabled. -->
@@ -170,8 +172,10 @@
     <string name="folder_closed">Folder closed</string>
     <!-- Folder renamed format -->
     <string name="folder_renamed">Folder renamed to <xliff:g id="name" example="Games">%1$s</xliff:g></string>
-    <!-- Folder name format -->
-    <string name="folder_name_format">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g></string>
+    <!-- Folder name format when folder has less than 4 items -->
+    <string name="folder_name_format_exact">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> items</string>
+    <!-- Folder name format when folder has 4 or more items shown in preview-->
+    <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 -->
@@ -180,7 +184,7 @@
     <string name="wallpaper_button_text">Wallpapers</string>
     <!-- Text for wallpaper change button -->
     <string name="styles_wallpaper_button_text">Styles &amp; wallpapers</string>
-    <!-- Text for settings button -->
+    <!-- Text for settings button [CHAR LIMIT=20]-->
     <string name="settings_button_text">Home settings</string>
     <!-- Message shown when a feature is disabled by the administrator -->
     <string name="msg_disabled_by_admin">Disabled by your admin</string>
@@ -205,8 +209,8 @@
     <!-- Summary for Notification dots setting. Tapping this will link enable/disable notification dots feature on the home screen. [CHAR LIMIT=50] -->
     <string name="notification_dots_service_title">Show notification dots</string>
 
-    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=40] -->
-    <string name="auto_add_shortcuts_label">Add icon to Home screen</string>
+    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=60] -->
+    <string name="auto_add_shortcuts_label">Add app icons to Home screen</string>
     <!-- Text description of the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=NONE] -->
     <string name="auto_add_shortcuts_description">For new apps</string>
 
@@ -314,6 +318,9 @@
     <!-- Accessibility action to dismiss a notification in the shortcuts menu for an icon. [CHAR_LIMIT=30] -->
     <string name="action_dismiss_notification">Dismiss</string>
 
+    <!-- Content description for arrow tip close button. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_close">Close</string>
+
     <!-- Accessibility confirmation for notification being dismissed. -->
     <string name="notification_dismissed">Notification dismissed</string>
 
@@ -325,17 +332,26 @@
 
     <!-- This string is in the work profile tab when a user has All Apps open on their phone. This is a label for a toggle to turn the work profile on and off. "Work profile" means a separate profile on a user's phone that's specifically for their work apps and managed by their company. "Work" is used as an adjective.-->
     <string name="work_profile_toggle_label">Work profile</string>
-    <!-- Title of an overlay in All Apps. This overlay is letting a user know about their work profile, which is managed by their employer. "Work apps" are apps in a user's work profile.-->
-    <string name="bottom_work_tab_user_education_title">Find work apps here</string>
-    <!-- Text in an overlay in All Apps. This overlay is letting a user know about their work profile, which is managed by their employer.-->
-    <string name="bottom_work_tab_user_education_body">Each work app has a badge and is kept secure by your organization. Move apps to your Home screen for easier access.</string>
-    <!-- This string is in the work profile tab when a user has All Apps open on their phone. It describes the label of a toggle, "Work profile," as being managed by the user's employer.
-    "Organization" is used to represent a variety of businesses, non-profits, and educational institutions).-->
-    <string name="work_mode_on_label">Managed by your organization</string>
-    <!-- This string appears under a the label of a toggle in the work profile tab on a user's phone. It describes the status of the toggle, "Work profile," when it's turned off. "Work profile" means a separate profile on a user's phone that's speficially for their work apps and is managed by their company.-->
-    <string name="work_mode_off_label">Notifications and apps are off</string>
-    <string name="bottom_work_tab_user_education_close_button">Close</string>
-    <string name="bottom_work_tab_user_education_closed">Closed</string>
+    <!--- User onboarding title for personal apps -->
+    <string name="work_profile_edu_personal_apps">Personal data is separate &amp; hidden from work apps</string>
+    <!--- User onboarding title for work profile apps -->
+    <string name="work_profile_edu_work_apps">Work apps &amp; data are visible to your IT admin</string>
+    <!-- Action label to proceed to the next work profile edu section-->
+    <string name="work_profile_edu_next">Next</string>
+    <!-- Action label to finish work profile edu-->
+    <string name="work_profile_edu_accept">Got it</string>
+
+    <!--- heading shown when user opens work apps tab while work apps are paused -->
+    <string name="work_apps_paused_title">Work profile is paused</string>
+    <!--- body shown when user opens work apps tab while work apps are paused -->
+    <string name="work_apps_paused_body">Work apps can\'t send you notifications, use your battery, or access your location</string>
+    <!-- content description for paused work apps list -->
+    <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
+
+
+
+    <!-- A tip shown pointing at work toggle -->
+    <string name="work_switch_tip">Pause work apps and notifications</string>
 
     <!-- Failed action error message: e.g. Failed: Pause -->
     <string name="remote_action_failed">Failed: <xliff:g id="what" example="Pause">%1$s</xliff:g></string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 80c791c..25f21f3 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -30,11 +30,13 @@
     </style>
 
     <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
-        <item name="allAppsScrimColor">#EAFFFFFF</item>
+        <item name="android:textColorSecondary">#DE000000</item>
+        <item name="allAppsScrimColor">#FFFFFFFF</item>
         <item name="allAppsInterimScrimAlpha">46</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
+        <item name="allAppsTheme">@style/AllAppsTheme</item>
         <item name="popupColorPrimary">#FFF</item>
-        <item name="popupColorSecondary">#F5F5F5</item> <!-- Gray 100 -->
+        <item name="popupColorSecondary">#F1F3F4</item>
         <item name="popupColorTertiary">#E0E0E0</item> <!-- Gray 300 -->
         <item name="isMainColorDark">false</item>
         <item name="isWorkspaceDarkText">false</item>
@@ -48,18 +50,27 @@
         <item name="folderFillColor">#CDFFFFFF</item>
         <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
         <item name="folderTextColor">#FF212121</item>
+        <item name="folderHintColor">#89616161</item>
         <item name="loadingIconColor">#CCFFFFFF</item>
+        <item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
+        <item name="workProfileOverlayTextColor">#FF212121</item>
+        <item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
+        <item name="disabledIconAlpha">.54</item>
 
         <item name="android:windowTranslucentStatus">false</item>
         <item name="android:windowTranslucentNavigation">false</item>
         <item name="android:windowDrawsSystemBarBackgrounds">true</item>
         <item name="android:statusBarColor">#00000000</item>
         <item name="android:navigationBarColor">#00000000</item>
+
+
     </style>
 
     <style name="LauncherTheme.DarkMainColor" parent="@style/LauncherTheme">
         <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
         <item name="folderTextColor">?attr/workspaceTextColor</item>
+        <item name="disabledIconAlpha">.254</item>
+
     </style>
 
     <style name="LauncherTheme.DarkText" parent="@style/LauncherTheme">
@@ -83,24 +94,30 @@
         <item name="android:textColorHint">#A0FFFFFF</item>
         <item name="android:colorControlHighlight">#A0FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
-        <item name="allAppsScrimColor">#EA212121</item>
+        <item name="allAppsScrimColor">#FF000000</item>
         <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">#5F6368</item> <!-- Gray 700 -->
+        <item name="popupColorSecondary">#202124</item>
         <item name="popupColorTertiary">#757575</item> <!-- Gray 600 -->
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="folderDotColor">#FF464646</item>
         <item name="folderFillColor">#DD3C4043</item> <!-- 87% GM2 800 -->
         <item name="folderIconBorderColor">#FF80868B</item>
         <item name="folderTextColor">@android:color/white</item>
+        <item name="folderHintColor">#89CCCCCC</item>
         <item name="isMainColorDark">true</item>
         <item name="loadingIconColor">#99FFFFFF</item>
+        <item name="iconOnlyShortcutColor">#B3FFFFFF</item>
+        <item name="workProfileOverlayTextColor">@android:color/white</item>
+        <item name="eduHalfSheetBGColor">#DD000000</item>
     </style>
 
     <style name="LauncherTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark">
         <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
         <item name="folderTextColor">@android:color/white</item>
+        <item name="disabledIconAlpha">.54</item>
     </style>
 
     <style name="LauncherTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark">
@@ -169,21 +186,32 @@
         <item name="android:importantForAccessibility">no</item>
     </style>
 
-    <!-- Base theme for BubbleTextView and sub classes -->
-    <style name="BaseIcon" parent="@android:style/TextAppearance.DeviceDefault">
+    <style name="AllAppsTheme">
+        <item name="disabledIconAlpha">.54</item>
+    </style>
+
+    <style name="AllAppsTheme.Dark">
+        <item name="disabledIconAlpha">.54</item>
+    </style>
+
+
+    <style name="BaseIconUnBounded" parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:layout_gravity">center</item>
         <item name="android:focusable">true</item>
         <item name="android:gravity">center_horizontal</item>
-        <item name="android:lines">1</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
         <item name="android:defaultFocusHighlightEnabled">false</item>
-
         <!-- No shadows in the base theme -->
         <item name="android:shadowRadius">0</item>
     </style>
 
+    <!-- Base theme for BubbleTextView and sub classes -->
+    <style name="BaseIcon" parent="BaseIconUnBounded">
+        <item name="android:lines">1</item>
+    </style>
+
     <!-- Icon displayed on the workspace -->
     <style name="BaseIcon.Workspace" >
         <item name="android:shadowRadius">2.0</item>
@@ -218,6 +246,7 @@
     <style name="DropTargetButton" parent="DropTargetButtonBase" />
 
     <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
+    <style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
 
     <style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
 
diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml
index 82547d5..1c99dfc 100644
--- a/res/xml/device_profiles.xml
+++ b/res/xml/device_profiles.xml
@@ -24,6 +24,7 @@
         launcher:numFolderRows="2"
         launcher:numFolderColumns="3"
         launcher:numHotseatIcons="3"
+        launcher:dbFile="launcher_3_by_3.db"
         launcher:defaultLayoutId="@xml/default_workspace_3x3" >
 
         <display-option
@@ -51,6 +52,7 @@
         launcher:numFolderRows="3"
         launcher:numFolderColumns="4"
         launcher:numHotseatIcons="4"
+        launcher:dbFile="launcher_4_by_4.db"
         launcher:defaultLayoutId="@xml/default_workspace_4x4" >
 
         <display-option
@@ -102,6 +104,7 @@
         launcher:numFolderRows="4"
         launcher:numFolderColumns="4"
         launcher:numHotseatIcons="5"
+        launcher:dbFile="launcher.db"
         launcher:defaultLayoutId="@xml/default_workspace_5x5" >
 
         <display-option
diff --git a/res/xml/dynamic_resources.xml b/res/xml/dynamic_resources.xml
new file mode 100644
index 0000000..f5d2628
--- /dev/null
+++ b/res/xml/dynamic_resources.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<DynamicResources>
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+
+
+</DynamicResources>
\ No newline at end of file
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
index 62915f2..3fa9b0a 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -19,21 +19,29 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := LauncherRoboTests
-LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+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-3.6.1-prebuilt
+    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)
 
 ############################################
@@ -42,16 +50,13 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := RunLauncherRoboTests
-LOCAL_SDK_VERSION := current
-LOCAL_JAVA_LIBRARIES := \
-    LauncherRoboTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_JAVA_LIBRARIES := LauncherRoboTests
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
 LOCAL_TEST_PACKAGE := Launcher3
-
-LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \
+LOCAL_INSTRUMENT_SOURCE_DIRS := packages/apps/Launcher3/src
 
 LOCAL_ROBOTEST_TIMEOUT := 36000
 
-include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk
+include prebuilts/misc/common/robolectric/4.3.1/run_robotests.mk
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index e0d6e53..a8e0cb3 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1,2 +1,19 @@
-manifest=packages/apps/Launcher3/AndroidManifest.xml
-sdk=26
+sdk=29
+shadows= \
+    com.android.launcher3.shadows.LShadowApplicationPackageManager \
+    com.android.launcher3.shadows.LShadowAppPredictionManager \
+    com.android.launcher3.shadows.LShadowAppWidgetManager \
+    com.android.launcher3.shadows.LShadowBackupManager \
+    com.android.launcher3.shadows.LShadowBitmap \
+    com.android.launcher3.shadows.LShadowDisplay \
+    com.android.launcher3.shadows.LShadowLauncherApps \
+    com.android.launcher3.shadows.LShadowTypeface \
+    com.android.launcher3.shadows.LShadowUserManager \
+    com.android.launcher3.shadows.LShadowWallpaperManager \
+    com.android.launcher3.shadows.ShadowDeviceFlag \
+    com.android.launcher3.shadows.ShadowLooperExecutor \
+    com.android.launcher3.shadows.ShadowMainThreadInitializedObject \
+    com.android.launcher3.shadows.ShadowOverrides \
+    com.android.launcher3.shadows.ShadowSurfaceTransactionApplier \
+
+application=com.android.launcher3.util.LauncherTestApplication
\ No newline at end of file
diff --git a/robolectric_tests/resources/cache_data_updated_task_data.txt b/robolectric_tests/resources/cache_data_updated_task_data.txt
index 302d58f..603dbe3 100644
--- a/robolectric_tests/resources/cache_data_updated_task_data.txt
+++ b/robolectric_tests/resources/cache_data_updated_task_data.txt
@@ -1,6 +1,6 @@
 # Model data used by CacheDataUpdatedTaskTest
 
-classMap s com.android.launcher3.WorkspaceItemInfo
+classMap s com.android.launcher3.model.data.WorkspaceItemInfo
 
 # Items for the BgDataModel
 
diff --git a/robolectric_tests/resources/package_install_state_change_task_data.txt b/robolectric_tests/resources/package_install_state_change_task_data.txt
index 4d63664..e82ea9d 100644
--- a/robolectric_tests/resources/package_install_state_change_task_data.txt
+++ b/robolectric_tests/resources/package_install_state_change_task_data.txt
@@ -1,7 +1,7 @@
 # Model data used by PackageInstallStateChangeTaskTest
 
-classMap s com.android.launcher3.WorkspaceItemInfo
-classMap w com.android.launcher3.LauncherAppWidgetInfo
+classMap s com.android.launcher3.model.data.WorkspaceItemInfo
+classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo
 
 # Items for the BgDataModel
 
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
deleted file mode 100644
index a3d1216..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.android.launcher3.config;
-
-
-import com.android.launcher3.config.BaseFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.robolectric.RuntimeEnvironment;
-
-import java.lang.annotation.Annotation;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Repeatable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Test rule that makes overriding flags in Robolectric tests easier. This rule clears all flags
- * before and after your test, avoiding one test method affecting subsequent methods.
- *
- * <p>Usage:
- * <pre>
- * {@literal @}Rule public final FlagOverrideRule flags = new FlagOverrideRule();
- *
- * {@literal @}FlagOverride(flag = "FOO", value=true)
- * {@literal @}Test public void myTest() {
- *     ...
- * }
- * </pre>
- */
-public final class FlagOverrideRule implements TestRule {
-
-    /**
-     * Container annotation for handling multiple {@link FlagOverride} annotations.
-     * <p>
-     * <p>Don't use this directly, use repeated {@link FlagOverride} annotations instead.
-     */
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target({ElementType.METHOD})
-    public @interface FlagOverrides {
-        FlagOverride[] value();
-    }
-
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target({ElementType.METHOD})
-    @Repeatable(FlagOverrides.class)
-    public @interface FlagOverride {
-        String key();
-
-        boolean value();
-    }
-
-    private boolean ruleInProgress;
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                FeatureFlags.initialize(RuntimeEnvironment.application.getApplicationContext());
-                ruleInProgress = true;
-                try {
-                    clearOverrides();
-                    applyAnnotationOverrides(description);
-                    base.evaluate();
-                } finally {
-                    ruleInProgress = false;
-                    clearOverrides();
-                }
-            }
-        };
-    }
-
-    private void override(BaseTogglableFlag flag, boolean newValue) {
-        if (!ruleInProgress) {
-            throw new IllegalStateException(
-                    "Rule isn't in progress. Did you remember to mark it with @Rule?");
-        }
-        flag.setForTests(newValue);
-    }
-
-    private void applyAnnotationOverrides(Description description) {
-        for (Annotation annotation : description.getAnnotations()) {
-            if (annotation.annotationType() == FlagOverride.class) {
-                applyAnnotation((FlagOverride) annotation);
-            } else if (annotation.annotationType() == FlagOverrides.class) {
-                // Note: this branch is hit if the annotation is repeated
-                for (FlagOverride flagOverride : ((FlagOverrides) annotation).value()) {
-                    applyAnnotation(flagOverride);
-                }
-            }
-        }
-    }
-
-    private void applyAnnotation(FlagOverride flagOverride) {
-        boolean found = false;
-        for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-            if (flag.getKey().equals(flagOverride.key())) {
-                override(flag, flagOverride.value());
-                found = true;
-                break;
-            }
-        }
-        if (!found) {
-            throw new IllegalStateException("Flag " + flagOverride.key() + " not found");
-        }
-    }
-
-    /**
-     * Resets all flags to their default values.
-     */
-    private void clearOverrides() {
-        for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-            flag.setForTests(flag.getDefaultValue());
-        }
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
deleted file mode 100644
index 1351348..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.android.launcher3.config;
-
-import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Sample Robolectric test that demonstrates flag-overriding.
- */
-@RunWith(RobolectricTestRunner.class)
-public class FlagOverrideSampleTest {
-
-    // Check out https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html for more information
-    // on @Rules.
-    @Rule
-    public final FlagOverrideRule flags = new FlagOverrideRule();
-
-    @FlagOverride(key = "EXAMPLE_FLAG", value = true)
-    @Test
-    public void withFlagOn() {
-        assertTrue(FeatureFlags.EXAMPLE_FLAG.get());
-    }
-
-
-    @FlagOverride(key = "EXAMPLE_FLAG", value = false)
-    @Test
-    public void withFlagOff() {
-        assertFalse(FeatureFlags.EXAMPLE_FLAG.get());
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
new file mode 100644
index 0000000..b7ba106
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+public final class FolderNameProviderTest {
+    private Context mContext;
+    private WorkspaceItemInfo mItem1;
+    private WorkspaceItemInfo mItem2;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mItem1 = new WorkspaceItemInfo(new AppInfo(
+                new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+                "title1",
+                UserHandle.of(10),
+                new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+        ));
+        mItem2 = new WorkspaceItemInfo(new AppInfo(
+                new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+                "title2",
+                UserHandle.of(10),
+                new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+        ));
+    }
+
+    @Test
+    public void getSuggestedFolderName_workAssignedToEnd() {
+        ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
+        list.add(mItem1);
+        list.add(mItem2);
+        FolderNameInfos nameInfos = new FolderNameInfos();
+        new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
+        assertEquals("Work", nameInfos.getLabels()[0]);
+
+        nameInfos.setLabel(0, "candidate1", 1.0f);
+        nameInfos.setLabel(1, "candidate2", 1.0f);
+        nameInfos.setLabel(2, "candidate3", 1.0f);
+        new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
+        assertEquals("Work", nameInfos.getLabels()[3]);
+        assertTrue(nameInfos.hasSuggestions());
+        assertTrue(nameInfos.hasPrimary());
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
index 096db57..c892618 100644
--- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
+++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -1,20 +1,22 @@
 package com.android.launcher3.logging;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.util.Scheduler;
 
 import java.io.File;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Calendar;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
 /**
  * Tests for {@link FileLog}
  */
@@ -22,9 +24,10 @@
 public class FileLogTest {
 
     private File mTempDir;
+    private boolean mTestActive;
 
     @Before
-    public void setUp() throws Exception {
+    public void setUp() {
         int count = 0;
         do {
             mTempDir = new File(RuntimeEnvironment.application.getCacheDir(),
@@ -32,14 +35,25 @@
         } while (!mTempDir.mkdir());
 
         FileLog.setDir(mTempDir);
+
+        mTestActive = true;
+        Scheduler scheduler = Shadows.shadowOf(FileLog.getHandler().getLooper()).getScheduler();
+        new Thread(() -> {
+            while (mTestActive) {
+                scheduler.advanceToLastPostedRunnable();
+            }
+        }).start();
     }
 
     @After
-    public void tearDown() throws Exception {
+    public void tearDown() {
         // Clear existing logs
-        new File(mTempDir, "log-0").delete();
-        new File(mTempDir, "log-1").delete();
+        for (int i = 0; i < FileLog.LOG_DAYS; i++) {
+            new File(mTempDir, "log-" + i).delete();
+        }
         mTempDir.delete();
+
+        mTestActive = false;
     }
 
     @Test
@@ -49,12 +63,12 @@
         }
         FileLog.print("Testing", "hoolalala");
         StringWriter writer = new StringWriter();
-        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(FileLog.flushAll(new PrintWriter(writer)));
         assertTrue(writer.toString().contains("hoolalala"));
 
         FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
         writer = new StringWriter();
-        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(FileLog.flushAll(new PrintWriter(writer)));
         assertTrue(writer.toString().contains("abracadabra"));
         // Exception is also printed
         assertTrue(writer.toString().contains("cat! cat!"));
@@ -70,17 +84,18 @@
         }
         FileLog.print("Testing", "hoolalala");
         StringWriter writer = new StringWriter();
-        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(FileLog.flushAll(new PrintWriter(writer)));
         assertTrue(writer.toString().contains("hoolalala"));
 
         Calendar threeDaysAgo = Calendar.getInstance();
         threeDaysAgo.add(Calendar.HOUR, -72);
-        new File(mTempDir, "log-0").setLastModified(threeDaysAgo.getTimeInMillis());
-        new File(mTempDir, "log-1").setLastModified(threeDaysAgo.getTimeInMillis());
+        for (int i = 0; i < FileLog.LOG_DAYS; i++) {
+            new File(mTempDir, "log-" + i).setLastModified(threeDaysAgo.getTimeInMillis());
+        }
 
         FileLog.print("Testing", "abracadabra", new Exception("cat! cat!"));
         writer = new StringWriter();
-        FileLog.flushAll(new PrintWriter(writer));
+        assertTrue(FileLog.flushAll(new PrintWriter(writer)));
         assertTrue(writer.toString().contains("abracadabra"));
         // Exception is also printed
         assertTrue(writer.toString().contains("cat! cat!"));
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index d7a2278..8aa6f37 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -4,54 +4,70 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.util.Pair;
 
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * Tests for {@link AddWorkspaceItemsTask}
  */
 @RunWith(RobolectricTestRunner.class)
-public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
+@LooperMode(Mode.PAUSED)
+public class AddWorkspaceItemsTaskTest {
 
     private final ComponentName mComponent1 = new ComponentName("a", "b");
     private final ComponentName mComponent2 = new ComponentName("b", "b");
 
-    private IntArray existingScreens;
-    private IntArray newScreens;
-    private IntSparseArrayMap<GridOccupancy> screenOccupancy;
+    private Context mTargetContext;
+    private InvariantDeviceProfile mIdp;
+    private LauncherAppState mAppState;
+    private LauncherModelHelper mModelHelper;
+
+    private IntArray mExistingScreens;
+    private IntArray mNewScreens;
+    private IntSparseArrayMap<GridOccupancy> mScreenOccupancy;
 
     @Before
-    public void initData() throws Exception {
-        existingScreens = new IntArray();
-        screenOccupancy = new IntSparseArrayMap<>();
-        newScreens = new IntArray();
+    public void setup() {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+        mIdp.numColumns = mIdp.numRows = 5;
+        mAppState = LauncherAppState.getInstance(mTargetContext);
 
-        idp.numColumns = 5;
-        idp.numRows = 5;
+        mExistingScreens = new IntArray();
+        mScreenOccupancy = new IntSparseArrayMap<>();
+        mNewScreens = new IntArray();
     }
 
     private AddWorkspaceItemsTask newTask(ItemInfo... items) {
@@ -70,17 +86,17 @@
         // Second screen has 2 holes of sizes 3x2 and 2x3
         setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
 
-        int[] spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
+        int[] spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
         assertEquals(2, spaceFound[0]);
-        assertTrue(screenOccupancy.get(spaceFound[0])
+        assertTrue(mScreenOccupancy.get(spaceFound[0])
                 .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
 
         // Find a larger space
-        spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
+        spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3);
         assertEquals(2, spaceFound[0]);
-        assertTrue(screenOccupancy.get(spaceFound[0])
+        assertTrue(mScreenOccupancy.get(spaceFound[0])
                 .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3));
     }
 
@@ -89,11 +105,11 @@
         // First screen has 2 holes of sizes 3x2 and 2x3
         setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
 
-        IntArray oldScreens = existingScreens.clone();
-        int[] spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
+        IntArray oldScreens = mExistingScreens.clone();
+        int[] spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3);
         assertFalse(oldScreens.contains(spaceFound[0]));
-        assertTrue(newScreens.contains(spaceFound[0]));
+        assertTrue(mNewScreens.contains(spaceFound[0]));
     }
 
     @Test
@@ -105,11 +121,14 @@
         setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
 
         // Nothing was added
-        assertTrue(executeTaskForTest(newTask(info)).isEmpty());
+        assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty());
     }
 
     @Test
     public void testAddItem_some_items_added() throws Exception {
+        Callbacks callbacks = mock(Callbacks.class);
+        mModelHelper.getModel().addCallbacks(callbacks);
+
         WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.intent = new Intent().setComponent(mComponent1);
 
@@ -119,7 +138,7 @@
         // Setup a screen with a hole
         setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
 
-        executeTaskForTest(newTask(info, info2)).get(0).run();
+        mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run();
         ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
         ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
 
@@ -134,18 +153,23 @@
     }
 
     private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception {
-        GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
-        occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
+        return mModelHelper.executeSimpleTask(
+                model -> writeWorkspaceWithHoles(model, startId, screenId, holes));
+    }
+
+    private int writeWorkspaceWithHoles(
+            BgDataModel bgDataModel, int startId, int screenId, Rect... holes) {
+        GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows);
+        occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true);
         for (Rect r : holes) {
             occupancy.markCells(r, false);
         }
 
-        existingScreens.add(screenId);
-        screenOccupancy.append(screenId, occupancy);
+        mExistingScreens.add(screenId);
+        mScreenOccupancy.append(screenId, occupancy);
 
-        ExecutorService executor = Executors.newSingleThreadExecutor();
-        for (int x = 0; x < idp.numColumns; x++) {
-            for (int y = 0; y < idp.numRows; y++) {
+        for (int x = 0; x < mIdp.numColumns; x++) {
+            for (int y = 0; y < mIdp.numRows; y++) {
                 if (!occupancy.cells[x][y]) {
                     continue;
                 }
@@ -157,20 +181,15 @@
                 info.cellX = x;
                 info.cellY = y;
                 info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-                bgDataModel.addItem(targetContext, info, false);
+                bgDataModel.addItem(mTargetContext, info, false);
 
-                executor.execute(() -> {
-                    ContentWriter writer = new ContentWriter(targetContext);
-                    info.writeToValues(writer);
-                    writer.put(Favorites._ID, info.id);
-                    targetContext.getContentResolver().insert(Favorites.CONTENT_URI,
-                            writer.getValues(targetContext));
-                });
+                ContentWriter writer = new ContentWriter(mTargetContext);
+                info.writeToValues(writer);
+                writer.put(Favorites._ID, info.id);
+                mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI,
+                        writer.getValues(mTargetContext));
             }
         }
-
-        executor.submit(() -> null).get();
-        executor.shutdown();
         return startId;
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
new file mode 100644
index 0000000..90313ab
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+
+import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.NO__ICON;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.util.ReflectionHelpers.setField;
+
+import android.app.backup.BackupManager;
+import android.content.pm.PackageInstaller;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.shadows.LShadowBackupManager;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.shadow.api.Shadow;
+
+/**
+ * Tests to verify backup and restore flow.
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(LooperMode.Mode.PAUSED)
+public class BackupRestoreTest {
+
+    private static final long MY_OLD_PROFILE_ID = 1;
+    private static final long MY_PROFILE_ID = 0;
+    private static final long OLD_WORK_PROFILE_ID = 11;
+    private static final int WORK_PROFILE_ID = 10;
+
+    private static final int SYSTEM_USER = 0;
+    private static final int FLAG_SYSTEM = 0x00000800;
+    private static final int FLAG_PROFILE = 0x00001000;
+
+    private LShadowUserManager mUserManager;
+    private BackupManager mBackupManager;
+    private LauncherModelHelper mModelHelper;
+    private SQLiteDatabase mDb;
+    private InvariantDeviceProfile mIdp;
+    private UserHandle mMainProfileUser;
+    private UserHandle mWorkProfileUser;
+
+    @Before
+    public void setUp() {
+        setupUserManager();
+        setupBackupManager();
+        mModelHelper = new LauncherModelHelper();
+        RestoreDbTask.setPending(RuntimeEnvironment.application, true);
+        mDb = mModelHelper.provider.getDb();
+        mIdp = InvariantDeviceProfile.INSTANCE.get(RuntimeEnvironment.application);
+    }
+
+    private void setupUserManager() {
+        final UserManager userManager = RuntimeEnvironment.application.getSystemService(
+                UserManager.class);
+        mUserManager = Shadow.extract(userManager);
+        // sign in to primary user
+        mMainProfileUser = mUserManager.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
+        // sign in to work profile
+        mWorkProfileUser = mUserManager.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
+    }
+
+    private void setupBackupManager() {
+        mBackupManager = new BackupManager(RuntimeEnvironment.application);
+        final LShadowBackupManager bm = Shadow.extract(mBackupManager);
+        bm.addProfile(MY_OLD_PROFILE_ID, mMainProfileUser);
+        bm.addProfile(OLD_WORK_PROFILE_ID, mWorkProfileUser);
+    }
+
+    @Test
+    public void testOnCreateDbIfNotExists_CreatesBackup() {
+        assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
+    }
+
+    @Test
+    public void testOnRestoreSessionWithValidCondition_PerformsRestore() throws Exception {
+        setupBackup();
+        verifyTableIsFilled(BACKUP_TABLE_NAME, false);
+        verifyTableIsEmpty(TABLE_NAME);
+        createRestoreSession();
+        verifyTableIsFilled(TABLE_NAME, true);
+    }
+
+    private void setupBackup() {
+        createTableUsingOldProfileId();
+        // setup grid for main user on first screen
+        mModelHelper.createGrid(new int[][][]{{
+                { APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
+                { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
+                { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
+                { APP_ICON, SHORTCUT, SHORTCUT, APP_ICON},
+            }}, 1, MY_OLD_PROFILE_ID);
+        // setup grid for work profile on second screen
+        mModelHelper.createGrid(new int[][][]{{
+                { NO__ICON, APP_ICON, SHORTCUT, SHORTCUT},
+                { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
+                { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
+                { APP_ICON, SHORTCUT, SHORTCUT, NO__ICON},
+            }}, 2, OLD_WORK_PROFILE_ID);
+        // simulates the creation of backup upon restore
+        new GridBackupTable(RuntimeEnvironment.application, mDb, mIdp.numHotseatIcons,
+                mIdp.numColumns, mIdp.numRows).doBackup(
+                        MY_OLD_PROFILE_ID, GridBackupTable.OPTION_REQUIRES_SANITIZATION);
+        // reset favorites table
+        createTableUsingOldProfileId();
+    }
+
+    private void verifyTableIsEmpty(String tableName) {
+        assertEquals(0, getCount(mDb, "SELECT * FROM " + tableName));
+    }
+
+    private void verifyTableIsFilled(String tableName, boolean sanitized) {
+        assertEquals(sanitized ? 12 : 13, getCount(mDb,
+                "SELECT * FROM " + tableName + " WHERE profileId = "
+                        + (sanitized ? MY_PROFILE_ID : MY_OLD_PROFILE_ID)));
+        assertEquals(10, getCount(mDb, "SELECT * FROM " + tableName + " WHERE profileId = "
+                + (sanitized ? WORK_PROFILE_ID : OLD_WORK_PROFILE_ID)));
+    }
+
+    private void createTableUsingOldProfileId() {
+        // simulates the creation of favorites table on old device
+        dropTable(mDb, TABLE_NAME);
+        addTableToDb(mDb, MY_OLD_PROFILE_ID, false);
+    }
+
+    private void createRestoreSession() throws Exception {
+        final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+        final PackageInstaller installer = RuntimeEnvironment.application.getPackageManager()
+                .getPackageInstaller();
+        final int sessionId = installer.createSession(params);
+        final PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId);
+        setField(info, "installReason", INSTALL_REASON_DEVICE_RESTORE);
+        // TODO: (b/148410677) we should verify the following call instead
+        //  InstallSessionHelper.INSTANCE.get(getContext()).restoreDbIfApplicable(info);
+        RestoreDbTask.restoreIfPossible(RuntimeEnvironment.application,
+                mModelHelper.provider.getHelper(), mBackupManager);
+    }
+
+    private static int getCount(SQLiteDatabase db, String sql) {
+        try (Cursor c = db.rawQuery(sql, null)) {
+            return c.getCount();
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
deleted file mode 100644
index c25df81..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.android.launcher3.model;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.sqlite.SQLiteDatabase;
-
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-public abstract class BaseGridChangesTestCase {
-
-
-    public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-    public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-
-    public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-    public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-    public static final int NO__ICON = -1;
-
-    public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
-
-    public Context mContext;
-    public TestLauncherProvider mProvider;
-    public SQLiteDatabase mDb;
-
-    @Before
-    public void setUpBaseCase() {
-        ShadowLog.stream = System.out;
-
-        mContext = RuntimeEnvironment.application;
-        mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
-        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
-        mDb = mProvider.getDb();
-    }
-
-    /**
-     * Adds a mock item in the DB.
-     * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
-     *             folder (where the type represents the number of items in the folder).
-     */
-    public int addItem(int type, int screen, int container, int x, int y) {
-        int id = LauncherSettings.Settings.call(mContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-
-        ContentValues values = new ContentValues();
-        values.put(LauncherSettings.Favorites._ID, id);
-        values.put(LauncherSettings.Favorites.CONTAINER, container);
-        values.put(LauncherSettings.Favorites.SCREEN, screen);
-        values.put(LauncherSettings.Favorites.CELLX, x);
-        values.put(LauncherSettings.Favorites.CELLY, y);
-        values.put(LauncherSettings.Favorites.SPANX, 1);
-        values.put(LauncherSettings.Favorites.SPANY, 1);
-
-        if (type == APP_ICON || type == SHORTCUT) {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
-            values.put(LauncherSettings.Favorites.INTENT,
-                    new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
-        } else {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE,
-                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
-            // Add folder items.
-            for (int i = 0; i < type; i++) {
-                addItem(APP_ICON, 0, id, 0, 0);
-            }
-        }
-
-        mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
-        return id;
-    }
-
-    public int[][][] createGrid(int[][][] typeArray) {
-        return createGrid(typeArray, 1);
-    }
-
-    /**
-     * Initializes the DB with mock elements to represent the provided grid structure.
-     * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
-     *                  type definitions. The first dimension represents the screens and the next
-     *                  two represent the workspace grid.
-     * @param startScreen First screen id from where the icons will be added.
-     * @return the same grid representation where each entry is the corresponding item id.
-     */
-    public int[][][] createGrid(int[][][] typeArray, int startScreen) {
-        LauncherSettings.Settings.call(mContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-        int[][][] ids = new int[typeArray.length][][];
-
-        for (int i = 0; i < typeArray.length; i++) {
-            // Add screen to DB
-            int screenId = startScreen + i;
-
-            // Keep the screen id counter up to date
-            LauncherSettings.Settings.call(mContext.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
-
-            ids[i] = new int[typeArray[i].length][];
-            for (int y = 0; y < typeArray[i].length; y++) {
-                ids[i][y] = new int[typeArray[i][y].length];
-                for (int x = 0; x < typeArray[i][y].length; x++) {
-                    if (typeArray[i][y][x] < 0) {
-                        // Empty cell
-                        ids[i][y][x] = -1;
-                    } else {
-                        ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
-                    }
-                }
-            }
-        }
-
-        return ids;
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
deleted file mode 100644
index bc936b7..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ /dev/null
@@ -1,220 +0,0 @@
-package com.android.launcher3.model;
-
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Color;
-import android.os.Process;
-import android.os.UserHandle;
-
-import com.android.launcher3.AppFilter;
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.mockito.ArgumentCaptor;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.function.Supplier;
-
-import androidx.annotation.NonNull;
-
-/**
- * Base class for writing tests for Model update tasks.
- */
-public class BaseModelUpdateTaskTestCase {
-
-    public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
-    private TestLauncherProvider mProvider;
-
-    public Context targetContext;
-    public UserHandle myUser;
-
-    public InvariantDeviceProfile idp;
-    public LauncherAppState appState;
-    public LauncherModel model;
-    public ModelWriter modelWriter;
-    public MyIconCache iconCache;
-
-    public BgDataModel bgDataModel;
-    public AllAppsList allAppsList;
-    public Callbacks callbacks;
-
-    @Before
-    public void setUp() throws Exception {
-        ShadowLog.stream = System.out;
-
-        mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
-        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
-
-        callbacks = mock(Callbacks.class);
-        appState = mock(LauncherAppState.class);
-        model = mock(LauncherModel.class);
-        modelWriter = mock(ModelWriter.class);
-
-        when(appState.getModel()).thenReturn(model);
-        when(model.getWriter(anyBoolean(), anyBoolean())).thenReturn(modelWriter);
-        when(model.getCallback()).thenReturn(callbacks);
-
-        myUser = Process.myUserHandle();
-
-        bgDataModel = new BgDataModel();
-        targetContext = RuntimeEnvironment.application;
-
-        idp = new InvariantDeviceProfile();
-        iconCache = new MyIconCache(targetContext, idp);
-
-        allAppsList = new AllAppsList(iconCache, new AppFilter());
-
-        when(appState.getIconCache()).thenReturn(iconCache);
-        when(appState.getInvariantDeviceProfile()).thenReturn(idp);
-        when(appState.getContext()).thenReturn(targetContext);
-    }
-
-    /**
-     * Synchronously executes the task and returns all the UI callbacks posted.
-     */
-    public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
-        when(model.isModelLoaded()).thenReturn(true);
-
-        Executor mockExecutor = mock(Executor.class);
-
-        task.init(appState, model, bgDataModel, allAppsList, mockExecutor);
-        task.run();
-        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-        verify(mockExecutor, atLeast(0)).execute(captor.capture());
-
-        return captor.getAllValues();
-    }
-
-    /**
-     * Initializes mock data for the test.
-     */
-    public void initializeData(String resourceName) throws Exception {
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                this.getClass().getResourceAsStream(resourceName)))) {
-            String line;
-            HashMap<String, Class> classMap = new HashMap<>();
-            while((line = reader.readLine()) != null) {
-                line = line.trim();
-                if (line.startsWith("#") || line.isEmpty()) {
-                    continue;
-                }
-                String[] commands = line.split(" ");
-                switch (commands[0]) {
-                    case "classMap":
-                        classMap.put(commands[1], Class.forName(commands[2]));
-                        break;
-                    case "bgItem":
-                        bgDataModel.addItem(targetContext,
-                                (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
-                        break;
-                    case "allApps":
-                        allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
-                        break;
-                }
-            }
-        }
-    }
-
-    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
-        HashMap<String, Field> cache = fieldCache.get(clazz);
-        if (cache == null) {
-            cache = new HashMap<>();
-            Class c = clazz;
-            while (c != null) {
-                for (Field f : c.getDeclaredFields()) {
-                    f.setAccessible(true);
-                    cache.put(f.getName(), f);
-                }
-                c = c.getSuperclass();
-            }
-            fieldCache.put(clazz, cache);
-        }
-
-        Object item = clazz.newInstance();
-        for (int i = startIndex; i < fieldDef.length; i++) {
-            String[] fieldData = fieldDef[i].split("=", 2);
-            Field f = cache.get(fieldData[0]);
-            Class type = f.getType();
-            if (type == int.class || type == long.class) {
-                f.set(item, Integer.parseInt(fieldData[1]));
-            } else if (type == CharSequence.class || type == String.class) {
-                f.set(item, fieldData[1]);
-            } else if (type == Intent.class) {
-                if (!fieldData[1].startsWith("#Intent")) {
-                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
-                }
-                f.set(item, Intent.parseUri(fieldData[1], 0));
-            } else if (type == ComponentName.class) {
-                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
-            } else {
-                throw new Exception("Added parsing logic for "
-                        + f.getName() + " of type " + f.getType());
-            }
-        }
-        return item;
-    }
-
-    public static class MyIconCache extends IconCache {
-
-        private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
-
-        public MyIconCache(Context context, InvariantDeviceProfile idp) {
-            super(context, idp);
-        }
-
-        @Override
-        protected <T> CacheEntry cacheLocked(
-                @NonNull ComponentName componentName,
-                UserHandle user, @NonNull Supplier<T> infoProvider,
-                @NonNull CachingLogic<T> cachingLogic,
-                boolean usePackageIcon, boolean useLowResIcon) {
-            CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
-            if (entry == null) {
-                entry = new CacheEntry();
-                getDefaultIcon(user).applyTo(entry);
-            }
-            return entry;
-        }
-
-        public void addCache(ComponentName key, String title) {
-            CacheEntry entry = new CacheEntry();
-            entry.icon = newIcon();
-            entry.color = Color.RED;
-            entry.title = title;
-            mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
-        }
-
-        public Bitmap newIcon() {
-            return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
-        }
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 42848f4..5610b0e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -2,19 +2,37 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.WorkspaceItemInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -23,61 +41,91 @@
  * Tests for {@link CacheDataUpdatedTask}
  */
 @RunWith(RobolectricTestRunner.class)
-public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
+@LooperMode(Mode.PAUSED)
+public class CacheDataUpdatedTaskTest {
 
     private static final String NEW_LABEL_PREFIX = "new-label-";
 
+    private LauncherModelHelper mModelHelper;
+
     @Before
-    public void initData() throws Exception {
-        initializeData("/cache_data_updated_task_data.txt");
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.initializeData("/cache_data_updated_task_data.txt");
+
         // Add dummy entries in the cache to simulate update
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
-            iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
+        Context context = RuntimeEnvironment.application;
+        IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
+        CachingLogic<ItemInfo> dummyLogic = new CachingLogic<ItemInfo>() {
+            @Override
+            public ComponentName getComponent(ItemInfo info) {
+                return info.getTargetComponent();
+            }
+
+            @Override
+            public UserHandle getUser(ItemInfo info) {
+                return info.user;
+            }
+
+            @Override
+            public CharSequence getLabel(ItemInfo info) {
+                return NEW_LABEL_PREFIX + info.id;
+            }
+
+            @NonNull
+            @Override
+            public BitmapInfo loadIcon(Context context, ItemInfo info) {
+                return BitmapInfo.of(Bitmap.createBitmap(1, 1, Config.ARGB_8888), Color.RED);
+            }
+        };
+
+        UserManager um = context.getSystemService(UserManager.class);
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+            iconCache.addIconToDBAndMemCache(info, dummyLogic, new PackageInfo(),
+                    um.getSerialNumberForUser(info.user), true);
         }
     }
 
     private CacheDataUpdatedTask newTask(int op, String... pkg) {
-        return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
+        return new CacheDataUpdatedTask(op, Process.myUserHandle(),
+                new HashSet<>(Arrays.asList(pkg)));
     }
 
     @Test
-    @Ignore("This test fails with resource errors") // b/131115553
     public void testCacheUpdate_update_apps() throws Exception {
         // Clear all icons from apps list so that its easy to check what was updated
-        for (AppInfo info : allAppsList.data) {
-            info.iconBitmap = null;
+        for (AppInfo info : mModelHelper.getAllAppsList().data) {
+            info.bitmap = BitmapInfo.LOW_RES_INFO;
         }
 
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
 
         // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
         // is not updated
         verifyUpdate(1, 2);
 
         // Verify that only app1 var updated in allAppsList
-        assertFalse(allAppsList.data.isEmpty());
-        for (AppInfo info : allAppsList.data) {
+        assertFalse(mModelHelper.getAllAppsList().data.isEmpty());
+        for (AppInfo info : mModelHelper.getAllAppsList().data) {
             if (info.componentName.getPackageName().equals("app1")) {
-                assertNotNull(info.iconBitmap);
+                assertFalse(info.bitmap.isNullOrLowRes());
             } else {
-                assertNull(info.iconBitmap);
+                assertTrue(info.bitmap.isNullOrLowRes());
             }
         }
     }
 
     @Test
-    @Ignore("This test fails with resource errors") // b/131115553
     public void testSessionUpdate_ignores_normal_apps() throws Exception {
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
 
         // app1 has no restored shortcuts. Verify that nothing was updated.
         verifyUpdate();
     }
 
     @Test
-    @Ignore("This test fails with resource errors") // b/131115553
     public void testSessionUpdate_updates_pending_apps() throws Exception {
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
 
         // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
         // were updated
@@ -86,13 +134,13 @@
 
     private void verifyUpdate(Integer... idsUpdated) {
         HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
             if (updates.contains(info.id)) {
                 assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
-                assertNotNull(((WorkspaceItemInfo) info).iconBitmap);
+                assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
             } else {
                 assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
-                assertNull(((WorkspaceItemInfo) info).iconBitmap);
+                assertTrue(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
             }
         }
     }
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index a46617e..bbbe21e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -130,7 +130,7 @@
         }
         helper.close();
 
-        helper = new DatabaseHelper(mContext, null, DB_FILE) {
+        helper = new DatabaseHelper(mContext, DB_FILE, false) {
             @Override
             public void onOpen(SQLiteDatabase db) { }
         };
@@ -161,7 +161,7 @@
 
         DbDowngradeHelper.updateSchemaFile(mSchemaFile, LauncherProvider.SCHEMA_VERSION, mContext);
 
-        DatabaseHelper dbHelper = new DatabaseHelper(mContext, null, DB_FILE) {
+        DatabaseHelper dbHelper = new DatabaseHelper(mContext, DB_FILE, false) {
             @Override
             public void onOpen(SQLiteDatabase db) { }
         };
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
new file mode 100644
index 0000000..8f3a83e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.ReflectionHelpers.setField;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageInstaller.SessionParams;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+
+/**
+ * Tests for layout parser for remote layout
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class DefaultLayoutProviderTest {
+
+    private LauncherModelHelper mModelHelper;
+    private Context mTargetContext;
+
+    @Before
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+
+        shadowOf(mTargetContext.getPackageManager())
+                .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE));
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
+                .putApp(TEST_PACKAGE, TEST_PACKAGE));
+
+        // Verify one item in hotseat
+        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_folder() throws Exception {
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .build());
+
+        // Verify folder
+        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
+        assertEquals(3, ((FolderInfo) info).contents.size());
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_folder_custom_title() throws Exception {
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder("CustomFolder")
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .build());
+
+        // Verify folder
+        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
+        assertEquals(3, ((FolderInfo) info).contents.size());
+        assertEquals("CustomFolder", info.title.toString());
+    }
+
+    @Test
+    public void testCustomProfileLoaded_with_widget() throws Exception {
+        String pendingAppPkg = "com.test.pending";
+
+        // Add a dummy session info so that the widget exists
+        SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+        params.setAppPackageName(pendingAppPkg);
+
+        PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
+        int sessionId = installer.createSession(params);
+        SessionInfo sessionInfo = installer.getSessionInfo(sessionId);
+        setField(sessionInfo, "installerPackageName", "com.test");
+        setField(sessionInfo, "appIcon", BitmapInfo.LOW_RES_ICON);
+
+        writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
+                .putWidget(pendingAppPkg, "DummyWidget", 2, 2));
+
+        // Verify widget
+        assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
+        ItemInfo info = mModelHelper.getBgDataModel().appWidgets.get(0);
+        assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
+        assertEquals(2, info.spanX);
+        assertEquals(2, info.spanY);
+    }
+
+    private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
+        mModelHelper.setupDefaultLayoutProvider(builder).loadModelSync();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
index 53287a9..56ce215 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
@@ -6,33 +6,53 @@
 import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.NO__ICON;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
+import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 /**
  * Unit tests for {@link GridBackupTable}
  */
 @RunWith(RobolectricTestRunner.class)
-public class GridBackupTableTest extends BaseGridChangesTestCase {
+public class GridBackupTableTest {
 
     private static final int BACKUP_ITEM_COUNT = 12;
 
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
+
     @Before
-    public void setupGridData() {
-        createGrid(new int[][][]{{
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
+        setupGridData();
+    }
+
+    private void setupGridData() {
+        mModelHelper.createGrid(new int[][][]{{
                 { APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
                 { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
                 { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
@@ -81,7 +101,7 @@
 
         assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
 
-        addItem(1, 2, DESKTOP, 1, 1);
+        mModelHelper.addItem(1, 2, DESKTOP, 1, 1);
         assertFalse(tableExists(mDb, BACKUP_TABLE_NAME));
     }
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 53f6a06..8e00dcb 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -1,25 +1,31 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.model.GridSizeMigrationTask.getWorkspaceScreenIds;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.content.Context;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.config.FlagOverrideRule;
 import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -28,33 +34,38 @@
  * Unit tests for {@link GridSizeMigrationTask}
  */
 @RunWith(RobolectricTestRunner.class)
-public class GridSizeMigrationTaskTest extends BaseGridChangesTestCase {
+public class GridSizeMigrationTaskTest {
 
-    @Rule
-    public final FlagOverrideRule flags = new FlagOverrideRule();
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
 
     private HashSet<String> mValidPackages;
     private InvariantDeviceProfile mIdp;
 
     @Before
     public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
         mValidPackages = new HashSet<>();
         mValidPackages.add(TEST_PACKAGE);
-        mIdp = new InvariantDeviceProfile();
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
     }
 
     @Test
     public void testHotseatMigration_apps_dropped() throws Exception {
         int[] hotseatItems = {
-                addItem(APP_ICON, 0, HOTSEAT, 0, 0),
-                addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
                 -1,
-                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
-                addItem(APP_ICON, 4, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0),
         };
 
         mIdp.numHotseatIcons = 3;
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
                 .migrateHotseat();
         // First item is dropped as it has the least weight.
         verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@@ -63,15 +74,15 @@
     @Test
     public void testHotseatMigration_shortcuts_dropped() throws Exception {
         int[] hotseatItems = {
-                addItem(APP_ICON, 0, HOTSEAT, 0, 0),
-                addItem(30, 1, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+                mModelHelper.addItem(30, 1, HOTSEAT, 0, 0),
                 -1,
-                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
-                addItem(10, 4, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+                mModelHelper.addItem(10, 4, HOTSEAT, 0, 0),
         };
 
         mIdp.numHotseatIcons = 3;
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
                 .migrateHotseat();
         // First item is dropped as it has the least weight.
         verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@@ -109,14 +120,14 @@
 
     @Test
     public void testWorkspace_empty_row_column_removed() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0, -1,  1},
                 {  3,  1, -1,  4},
                 { -1, -1, -1, -1},
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Column 2 and row 2 got removed.
@@ -129,14 +140,14 @@
 
     @Test
     public void testWorkspace_new_screen_created() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column get moved to new screen
@@ -151,7 +162,7 @@
 
     @Test
     public void testWorkspace_items_merged_in_next_screen() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
@@ -161,7 +172,7 @@
                 {  3,  1, -1,  4},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on the 3rd
@@ -179,9 +190,9 @@
 
     @Test
     public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
-        // First screen has 2 items that need to be moved, but second screen has only one
+        // First screen has 2 mItems that need to be moved, but second screen has only one
         // empty space after migration (top-left corner)
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
@@ -193,7 +204,7 @@
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -217,14 +228,14 @@
         }
         // The first screen has one item on the 4th column which needs moving, as the first row
         // will be kept empty.
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 { -1, -1, -1, -1},
                 {  3,  1,  7,  0},
                 {  8,  7,  7, -1},
                 {  5,  2,  7, -1},
         }}, 0);
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 4)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -244,14 +255,14 @@
             return;
         }
         // Items will get moved to the next screen to keep the first screen empty.
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 { -1, -1, -1, -1},
                 {  0,  1,  0,  0},
                 {  8,  7,  7, -1},
                 {  5,  6,  7, -1},
         }}, 0);
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -266,12 +277,12 @@
     }
 
     /**
-     * Verifies that the workspace items are arranged in the provided order.
+     * Verifies that the workspace mItems are arranged in the provided order.
      * @param ids A 3d array where the first dimension represents the screen, and the rest two
      *            represent the workspace grid.
      */
     private void verifyWorkspace(int[][][] ids) {
-        IntArray allScreens = getWorkspaceScreenIds(mDb);
+        IntArray allScreens = getWorkspaceScreenIds(mDb, LauncherSettings.Favorites.TABLE_NAME);
         assertEquals(ids.length, allScreens.size());
         int total = 0;
 
@@ -340,7 +351,7 @@
         private final LinkedList<Point> mPoints;
 
         public MultiStepMigrationTaskVerifier(int... points) {
-            super(null, null, null);
+            super(null, null, null, false);
 
             mPoints = new LinkedList<>();
             for (int i = 0; i < points.length; i += 2) {
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
new file mode 100644
index 0000000..cca333c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_CONTENT_URI;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.os.Process;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.HashSet;
+
+/** Unit tests for {@link GridSizeMigrationTaskV2} */
+@RunWith(RobolectricTestRunner.class)
+public class GridSizeMigrationTaskV2Test {
+
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
+
+    private HashSet<String> mValidPackages;
+    private InvariantDeviceProfile mIdp;
+
+    private final String testPackage1 = "com.android.launcher3.validpackage1";
+    private final String testPackage2 = "com.android.launcher3.validpackage2";
+    private final String testPackage3 = "com.android.launcher3.validpackage3";
+    private final String testPackage4 = "com.android.launcher3.validpackage4";
+    private final String testPackage5 = "com.android.launcher3.validpackage5";
+    private final String testPackage6 = "com.android.launcher3.validpackage6";
+    private final String testPackage7 = "com.android.launcher3.validpackage7";
+    private final String testPackage8 = "com.android.launcher3.validpackage8";
+    private final String testPackage9 = "com.android.launcher3.validpackage9";
+    private final String testPackage10 = "com.android.launcher3.validpackage10";
+
+    @Before
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
+        mValidPackages = new HashSet<>();
+        mValidPackages.add(TEST_PACKAGE);
+        mValidPackages.add(testPackage1);
+        mValidPackages.add(testPackage2);
+        mValidPackages.add(testPackage3);
+        mValidPackages.add(testPackage4);
+        mValidPackages.add(testPackage5);
+        mValidPackages.add(testPackage6);
+        mValidPackages.add(testPackage7);
+        mValidPackages.add(testPackage8);
+        mValidPackages.add(testPackage9);
+        mValidPackages.add(testPackage10);
+
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                Process.myUserHandle());
+        dropTable(mDb, LauncherSettings.Favorites.TMP_TABLE);
+        LauncherSettings.Favorites.addTableToDb(mDb, userSerial, false,
+                LauncherSettings.Favorites.TMP_TABLE);
+    }
+
+    @Test
+    public void testMigration() {
+        int[] srcHotseatItems = {
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+                -1,
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+                mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+        };
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage5, 5, TMP_CONTENT_URI);
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage6, 6, TMP_CONTENT_URI);
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage8, 8, TMP_CONTENT_URI);
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage9, 9, TMP_CONTENT_URI);
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 3, testPackage10, 10, TMP_CONTENT_URI);
+
+        int[] destHotseatItems = {
+                -1,
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2),
+                -1,
+        };
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
+
+        mIdp.numHotseatIcons = 4;
+        mIdp.numColumns = 4;
+        mIdp.numRows = 4;
+        GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+                LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages,
+                srcHotseatItems.length);
+        GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+                LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages,
+                mIdp.numHotseatIcons);
+        GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+                destReader, mIdp.numHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+        task.migrate();
+
+        // Check hotseat items
+        Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+                new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+                "container=" + CONTAINER_HOTSEAT, null, null, null);
+        assertEquals(c.getCount(), mIdp.numHotseatIcons);
+        int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+        int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 1);
+        assertTrue(c.getString(intentIndex).contains(testPackage2));
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 0);
+        assertTrue(c.getString(intentIndex).contains(testPackage1));
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 2);
+        assertTrue(c.getString(intentIndex).contains(testPackage3));
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 3);
+        assertTrue(c.getString(intentIndex).contains(testPackage4));
+        c.close();
+
+        // Check workspace items
+        c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+                new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY,
+                        LauncherSettings.Favorites.INTENT},
+                "container=" + CONTAINER_DESKTOP, null, null, null);
+        assertEquals(c.getCount(), 6);
+        intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+        int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX);
+        int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY);
+
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage7));
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage6));
+        assertEquals(c.getInt(cellXIndex), 0);
+        assertEquals(c.getInt(cellYIndex), 3);
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage10));
+        assertEquals(c.getInt(cellXIndex), 1);
+        assertEquals(c.getInt(cellYIndex), 3);
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage5));
+        assertEquals(c.getInt(cellXIndex), 2);
+        assertEquals(c.getInt(cellYIndex), 3);
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage9));
+        assertEquals(c.getInt(cellXIndex), 3);
+        assertEquals(c.getInt(cellYIndex), 3);
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage8));
+        assertEquals(c.getInt(cellXIndex), 0);
+        assertEquals(c.getInt(cellYIndex), 2);
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
similarity index 72%
rename from tests/src/com/android/launcher3/model/LoaderCursorTest.java
rename to robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index b13468d..0e760f9 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.launcher3.model;
 
 import static com.android.launcher3.LauncherSettings.Favorites.CELLX;
@@ -17,6 +33,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.SCREEN;
 import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
 import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -24,69 +41,59 @@
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.database.MatrixCursor;
-import android.graphics.Bitmap;
 import android.os.Process;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.PackageManagerHelper;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 /**
  * Tests for {@link LoaderCursor}
  */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
 public class LoaderCursorTest {
 
-    private LauncherAppState mMockApp;
-    private IconCache mMockIconCache;
+    private LauncherAppState mApp;
 
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
     private Context mContext;
-    private LauncherAppsCompat mLauncherApps;
 
     private LoaderCursor mLoaderCursor;
 
     @Before
     public void setup() {
-        mIDP = new InvariantDeviceProfile();
+        mContext = RuntimeEnvironment.application;
+        mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
+        mApp = LauncherAppState.getInstance(mContext);
+
         mCursor = new MatrixCursor(new String[] {
                 ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
                 _ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
-        mContext = InstrumentationRegistry.getTargetContext();
 
-        mMockApp = mock(LauncherAppState.class);
-        mMockIconCache = mock(IconCache.class);
-        when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
-        when(mMockApp.getInvariantDeviceProfile()).thenReturn(mIDP);
-        when(mMockApp.getContext()).thenReturn(mContext);
-        mLauncherApps = LauncherAppsCompat.getInstance(mContext);
-
-        mLoaderCursor = new LoaderCursor(mCursor, mMockApp);
+        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp,
+                new UserManagerState());
         mLoaderCursor.allUsers.put(0, Process.myUserHandle());
     }
 
@@ -109,28 +116,33 @@
     }
 
     @Test
-    public void getAppShortcutInfo_dontAllowMissing_validComponent() {
+    public void getAppShortcutInfo_dontAllowMissing_validComponent() throws Exception {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_PACKAGE);
+        shadowOf(mContext.getPackageManager()).addActivityIfNotPresent(cn);
+
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
 
-        ComponentName cn = mLauncherApps.getActivityList(null, mLoaderCursor.user)
-                .get(0).getComponentName();
-        WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
-                new Intent().setComponent(cn), false /* allowMissingTarget */, true);
+        WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+                mLoaderCursor.getAppShortcutInfo(
+                        new Intent().setComponent(cn), false  /* allowMissingTarget */, true))
+                .get();
         assertNotNull(info);
-        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
     }
 
     @Test
-    public void getAppShortcutInfo_allowMissing_invalidComponent() {
+    public void getAppShortcutInfo_allowMissing_invalidComponent() throws Exception {
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
 
         ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
-        WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
-                new Intent().setComponent(cn), true  /* allowMissingTarget */, true);
+        WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+                mLoaderCursor.getAppShortcutInfo(
+                        new Intent().setComponent(cn), true  /* allowMissingTarget */, true))
+                .get();
         assertNotNull(info);
-        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
     }
 
     @Test
@@ -138,11 +150,8 @@
         initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
         assertTrue(mLoaderCursor.moveToNext());
 
-        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
-        when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
-                .thenReturn(BitmapInfo.fromBitmap(icon));
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
-        assertEquals(icon, info.iconBitmap);
+        assertTrue(mApp.getIconCache().isDefaultIcon(info.bitmap, info.user));
         assertEquals("my-shortcut", info.title);
         assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
     }
@@ -164,7 +173,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Overlapping items are not placed
+        // Overlapping mItems are not placed
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
         assertFalse(mLoaderCursor.checkItemPlacement(
@@ -190,7 +199,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Hotseat items are only placed based on screenId
+        // Hotseat mItems are only placed based on screenId
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
         assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000..87fe3c0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Process;
+
+import com.android.launcher3.PagedView;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class ModelMultiCallbacksTest {
+
+    private LauncherModelHelper mModelHelper;
+
+    private ShadowPackageManager mSpm;
+    private LooperExecutor mTempMainExecutor;
+
+    @Before
+    public void setUp() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.installApp(TEST_PACKAGE);
+
+        mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+
+        // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+        // so that we can wait appropriately for the loader to complete.
+        mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
+        ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_loadedTogether() throws Exception {
+        setupWorkspacePages(3);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+
+        // Add a new callback
+        cb1.reset();
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        cb2.mPageToBindSync = 2;
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        cb2.verifySynchronouslyBound(3);
+
+        // Remove callbacks
+        cb1.reset();
+        cb2.reset();
+
+        // No effect on callbacks when removing an callback
+        mModelHelper.getModel().removeCallbacks(cb2);
+        waitForLoaderAndTempMainThread();
+        assertNull(cb1.mDeferredExecutor);
+        assertNull(cb2.mDeferredExecutor);
+
+        // Reloading only loads registered callbacks
+        mModelHelper.getModel().startLoader();
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        assertNull(cb2.mDeferredExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_receiveUpdates() throws Exception {
+        setupWorkspacePages(1);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+        waitForLoaderAndTempMainThread();
+
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE);
+
+        // Install package 1
+        String pkg1 = "com.test.pkg1";
+        mModelHelper.installApp(pkg1);
+        mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1);
+        cb2.verifyApps(TEST_PACKAGE, pkg1);
+
+        // Install package 2
+        String pkg2 = "com.test.pkg2";
+        mModelHelper.installApp(pkg2);
+        mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+
+        // Uninstall package 2
+        mSpm.removePackage(pkg1);
+        mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+
+        // Unregister a callback and verify updates no longer received
+        mModelHelper.getModel().removeCallbacks(cb2);
+        mSpm.removePackage(pkg2);
+        mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+    }
+
+    private void waitForLoaderAndTempMainThread() throws Exception {
+        Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        mTempMainExecutor.submit(() -> { }).get();
+    }
+
+    private void setupWorkspacePages(int pageCount) throws Exception {
+        // Create a layout with 3 pages
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+        for (int i = 0; i < pageCount; i++) {
+            builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+        }
+        mModelHelper.setupDefaultLayoutProvider(builder);
+    }
+
+    private abstract static class MyCallbacks implements Callbacks {
+
+        final List<ItemInfo> mItems = new ArrayList<>();
+        int mPageToBindSync = 0;
+        int mPageBoundSync = PagedView.INVALID_PAGE;
+        ViewOnDrawExecutor mDeferredExecutor;
+        AppInfo[] mAppInfos;
+
+        MyCallbacks() { }
+
+        @Override
+        public void onPageBoundSynchronously(int page) {
+            mPageBoundSync = page;
+        }
+
+        @Override
+        public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+            mDeferredExecutor = executor;
+        }
+
+        @Override
+        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+            mItems.addAll(shortcuts);
+        }
+
+        @Override
+        public void bindAllApplications(AppInfo[] apps, int flags) {
+            mAppInfos = apps;
+        }
+
+        @Override
+        public int getPageToBindSynchronously() {
+            return mPageToBindSync;
+        }
+
+        public void reset() {
+            mItems.clear();
+            mPageBoundSync = PagedView.INVALID_PAGE;
+            mDeferredExecutor = null;
+            mAppInfos = null;
+        }
+
+        public void verifySynchronouslyBound(int totalItems) {
+            // Verify that the requested page is bound synchronously
+            assertEquals(mPageBoundSync, mPageToBindSync);
+            assertEquals(mItems.size(), 1);
+            assertEquals(mItems.get(0).screenId, mPageBoundSync);
+            assertNotNull(mDeferredExecutor);
+
+            // Verify that all other pages are bound properly
+            mDeferredExecutor.runAllTasks();
+            assertEquals(mItems.size(), totalItems);
+        }
+
+        public void verifyApps(String... apps) {
+            assertEquals(apps.length, mAppInfos.length);
+            assertEquals(Arrays.stream(mAppInfos)
+                    .map(ai -> ai.getTargetComponent().getPackageName())
+                    .collect(Collectors.toSet()),
+                    new HashSet<>(Arrays.asList(apps)));
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index 42a4f5c..e43df21 100644
--- a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -2,16 +2,18 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+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.pm.PackageInstallInfo;
+import com.android.launcher3.util.LauncherModelHelper;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -20,15 +22,19 @@
  * Tests for {@link PackageInstallStateChangedTask}
  */
 @RunWith(RobolectricTestRunner.class)
-public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
+@LooperMode(Mode.PAUSED)
+public class PackageInstallStateChangedTaskTest {
+
+    private LauncherModelHelper mModelHelper;
 
     @Before
-    public void initData() throws Exception {
-        initializeData("/package_install_state_change_task_data.txt");
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.initializeData("/package_install_state_change_task_data.txt");
     }
 
     private PackageInstallStateChangedTask newTask(String pkg, int progress) {
-        int state = PackageInstallerCompat.STATUS_INSTALLING;
+        int state = PackageInstallInfo.STATUS_INSTALLING;
         PackageInstallInfo installInfo = new PackageInstallInfo(pkg, state, progress,
                 android.os.Process.myUserHandle());
         return new PackageInstallStateChangedTask(installInfo);
@@ -36,7 +42,7 @@
 
     @Test
     public void testSessionUpdate_ignore_installed() throws Exception {
-        executeTaskForTest(newTask("app1", 30));
+        mModelHelper.executeTaskForTest(newTask("app1", 30));
 
         // No shortcuts were updated
         verifyProgressUpdate(0);
@@ -44,21 +50,21 @@
 
     @Test
     public void testSessionUpdate_shortcuts_updated() throws Exception {
-        executeTaskForTest(newTask("app3", 30));
+        mModelHelper.executeTaskForTest(newTask("app3", 30));
 
         verifyProgressUpdate(30, 5, 6, 7);
     }
 
     @Test
     public void testSessionUpdate_widgets_updated() throws Exception {
-        executeTaskForTest(newTask("app4", 30));
+        mModelHelper.executeTaskForTest(newTask("app4", 30));
 
         verifyProgressUpdate(30, 8, 9);
     }
 
     private void verifyProgressUpdate(int progress, Integer... idsUpdated) {
         HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
             if (info instanceof WorkspaceItemInfo) {
                 assertEquals(updates.contains(info.id) ? progress: 0,
                         ((WorkspaceItemInfo) info).getInstallProgress());
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
similarity index 78%
rename from tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
rename to robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 856db55..4184d33 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.launcher3.provider;
 
 import static org.junit.Assert.assertEquals;
@@ -6,21 +21,18 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 /**
  * Tests for {@link RestoreDbTask}
  */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
 public class RestoreDbTaskTest {
 
     @Test
@@ -83,7 +95,7 @@
         private final long mProfileId;
 
         MyDatabaseHelper(long profileId) {
-            super(InstrumentationRegistry.getContext(), null, null);
+            super(RuntimeEnvironment.application, null, false);
             mProfileId = profileId;
         }
 
diff --git a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
new file mode 100644
index 0000000..0ca5ce6
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.secondarydisplay;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.LauncherUIHelper.doLayout;
+import static com.android.launcher3.util.Preconditions.assertNotNull;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.shadows.ShadowOverrides;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowUserManager;
+
+/**
+ * Tests for {@link SecondaryDisplayLauncher} with work profile
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class SDWorkModeTest {
+
+    private static final int SYSTEM_USER = 0;
+    private static final int FLAG_SYSTEM = 0x00000800;
+    private static final int WORK_PROFILE_ID = 10;
+    private static final int FLAG_PROFILE = 0x00001000;
+
+    private Context mTargetContext;
+    private InvariantDeviceProfile mIdp;
+    private LauncherModelHelper mModelHelper;
+
+    private LauncherLayoutBuilder mLayoutBuilder;
+
+    @Before
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+        ShadowOverrides.setProvider(UserEventDispatcher.class,
+                c -> mock(UserEventDispatcher.class));
+        Settings.Global.putFloat(mTargetContext.getContentResolver(),
+                Settings.Global.WINDOW_ANIMATION_SCALE, 0);
+
+        mModelHelper.installApp(TEST_PACKAGE);
+        mLayoutBuilder = new LauncherLayoutBuilder();
+    }
+
+    @Test
+    public void testAllAppsList_noWorkProfile() throws Exception {
+        SecondaryDisplayLauncher launcher = loadLauncher();
+        launcher.showAppDrawer(true);
+        doLayout(launcher);
+
+        verifyRecyclerViewCount(launcher.getAppsView().getActiveRecyclerView());
+    }
+
+    @Test
+    public void testAllAppsList_workProfile() throws Exception {
+        ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class));
+        sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
+        sum.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
+
+        SecondaryDisplayLauncher launcher = loadLauncher();
+        launcher.showAppDrawer(true);
+        doLayout(launcher);
+
+        AllAppsRecyclerView rv1 = launcher.getAppsView().getActiveRecyclerView();
+        verifyRecyclerViewCount(rv1);
+
+        assertNotNull(launcher.getAppsView().getWorkModeSwitch());
+        assertTrue(launcher.getAppsView().getRecyclerViewContainer() instanceof AllAppsPagedView);
+
+        AllAppsPagedView pagedView =
+                (AllAppsPagedView) launcher.getAppsView().getRecyclerViewContainer();
+        pagedView.snapToPageImmediately(1);
+        doLayout(launcher);
+
+        AllAppsRecyclerView rv2 = launcher.getAppsView().getActiveRecyclerView();
+        verifyRecyclerViewCount(rv2);
+        assertNotSame(rv1, rv2);
+    }
+
+    private SecondaryDisplayLauncher loadLauncher() throws Exception {
+        // Install 100 apps
+        for (int i = 0; i < 100; i++) {
+            mModelHelper.installApp(TEST_PACKAGE + i);
+        }
+        mModelHelper.setupDefaultLayoutProvider(new LauncherLayoutBuilder()).loadModelSync();
+        SecondaryDisplayLauncher launcher =
+                Robolectric.buildActivity(SecondaryDisplayLauncher.class).setup().get();
+        doLayout(launcher);
+        return launcher;
+    }
+
+    private void verifyRecyclerViewCount(AllAppsRecyclerView rv) {
+        int childCount = rv.getChildCount();
+        assertTrue(childCount > 0);
+        assertTrue(childCount < 100);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppPredictionManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppPredictionManager.java
new file mode 100644
index 0000000..ae051f7
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppPredictionManager.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shadows;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for {@link AppPredictionManager} which create mock predictors
+ */
+@Implements(value = AppPredictionManager.class)
+public class LShadowAppPredictionManager {
+
+    @Implementation
+    public AppPredictor createAppPredictionSession(AppPredictionContext predictionContext) {
+        return mock(AppPredictor.class);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
new file mode 100644
index 0000000..696ffd0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.os.Process;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowAppWidgetManager;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Extension of {@link ShadowAppWidgetManager} with missing shadow methods
+ */
+@Implements(value = AppWidgetManager.class)
+public class LShadowAppWidgetManager extends ShadowAppWidgetManager {
+
+    @Override
+    protected List<AppWidgetProviderInfo> getInstalledProviders() {
+        return getInstalledProvidersForProfile(null);
+    }
+
+    @Implementation
+    public List<AppWidgetProviderInfo> getInstalledProvidersForProfile(UserHandle profile) {
+        UserHandle user = profile == null ? Process.myUserHandle() : profile;
+        return super.getInstalledProviders().stream().filter(
+                info -> user.equals(info.getProfile())).collect(Collectors.toList());
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
new file mode 100644
index 0000000..da8b919
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
@@ -0,0 +1,36 @@
+/*
+ * 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.shadows;
+
+import android.os.Process;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+
+/**
+ * Shadow for {@link ShadowApplicationPackageManager} which create mock predictors
+ */
+@Implements(className = "android.app.ApplicationPackageManager")
+public class LShadowApplicationPackageManager extends ShadowApplicationPackageManager {
+
+    @Implementation
+    public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
+        return Process.myUserHandle().equals(user) ? label : "Work " + label;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBackupManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBackupManager.java
new file mode 100644
index 0000000..eae0101
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBackupManager.java
@@ -0,0 +1,45 @@
+/*
+ * 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.shadows;
+
+import android.app.backup.BackupManager;
+import android.os.UserHandle;
+import android.util.LongSparseArray;
+
+import androidx.annotation.Nullable;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowBackupManager;
+
+/**
+ * Extension of {@link ShadowBackupManager} with missing shadow methods
+ */
+@Implements(value = BackupManager.class)
+public class LShadowBackupManager extends ShadowBackupManager {
+
+    private LongSparseArray<UserHandle> mProfileMapping = new LongSparseArray<>();
+
+    public void addProfile(long userSerial, UserHandle userHandle) {
+        mProfileMapping.put(userSerial, userHandle);
+    }
+
+    @Implementation
+    @Nullable
+    public UserHandle getUserForAncestralSerialNumber(long ancestralSerialNumber) {
+        return mProfileMapping.get(ancestralSerialNumber);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
new file mode 100644
index 0000000..abd90bb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowBitmap;
+
+/**
+ * Extension of {@link ShadowBitmap} with missing shadow methods
+ */
+@Implements(value = Bitmap.class)
+public class LShadowBitmap extends ShadowBitmap {
+
+    @Implementation
+    protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+        return extractAlpha();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java
new file mode 100644
index 0000000..3813fa1
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowDisplay.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shadows;
+
+import static org.robolectric.shadow.api.Shadow.directlyOn;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Display;
+
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadows.ShadowDisplay;
+
+/**
+ * Extension of {@link ShadowDisplay} with missing shadow methods
+ */
+@Implements(value = Display.class)
+public class LShadowDisplay extends ShadowDisplay {
+
+    private final Rect mInsets = new Rect();
+
+    @RealObject Display realObject;
+
+    /**
+     * Sets the insets for the display
+     */
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+    }
+
+    @Override
+    protected void getCurrentSizeRange(Point outSmallestSize, Point outLargestSize) {
+        directlyOn(realObject, Display.class).getCurrentSizeRange(outSmallestSize, outLargestSize);
+        outSmallestSize.x -= mInsets.left + mInsets.right;
+        outLargestSize.x -= mInsets.left + mInsets.right;
+
+        outSmallestSize.y -= mInsets.top + mInsets.bottom;
+        outLargestSize.y -= mInsets.top + mInsets.bottom;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
new file mode 100644
index 0000000..6a6f0fb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import static org.robolectric.util.ReflectionHelpers.ClassParameter;
+import static org.robolectric.util.ReflectionHelpers.callConstructor;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowLauncherApps;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * Extension of {@link ShadowLauncherApps} with missing shadow methods
+ */
+@Implements(value = LauncherApps.class)
+public class LShadowLauncherApps extends ShadowLauncherApps {
+
+    public final ArraySet<PackageUserKey> disabledApps = new ArraySet<>();
+    public final ArraySet<ComponentKey> disabledActivities = new ArraySet<>();
+
+    @Implementation
+    @Override
+    protected List<ShortcutInfo> getShortcuts(LauncherApps.ShortcutQuery query, UserHandle user) {
+        try {
+            return super.getShortcuts(query, user);
+        } catch (UnsupportedOperationException e) {
+            return Collections.emptyList();
+        }
+    }
+
+    @Implementation
+    protected boolean isPackageEnabled(String packageName, UserHandle user) {
+        return !disabledApps.contains(new PackageUserKey(packageName, user));
+    }
+
+    @Implementation
+    protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
+        return !disabledActivities.contains(new ComponentKey(component, user));
+    }
+
+    @Implementation
+    protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
+        ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
+                .resolveActivity(intent, 0);
+        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo, user);
+    }
+
+    public LauncherActivityInfo getLauncherActivityInfo(
+            ActivityInfo activityInfo, UserHandle user) {
+        return callConstructor(LauncherActivityInfo.class,
+                ClassParameter.from(Context.class, RuntimeEnvironment.application),
+                ClassParameter.from(ActivityInfo.class, activityInfo),
+                ClassParameter.from(UserHandle.class, user));
+    }
+
+    @Implementation
+    public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user)
+            throws PackageManager.NameNotFoundException {
+        return RuntimeEnvironment.application.getPackageManager()
+                .getApplicationInfo(packageName, flags);
+    }
+
+    @Implementation
+    public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER)
+                .setPackage(packageName);
+        return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+                .stream()
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
+                .collect(Collectors.toList());
+    }
+
+    @Implementation
+    public boolean hasShortcutHostPermission() {
+        return true;
+    }
+
+    @Implementation
+    public List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions() {
+        return RuntimeEnvironment.application.getPackageManager().getPackageInstaller()
+                .getAllSessions();
+    }
+
+    @Implementation
+    public void registerPackageInstallerSessionCallback(
+            Executor executor, PackageInstaller.SessionCallback callback) {
+    }
+
+    @Override
+    protected List<LauncherActivityInfo> getShortcutConfigActivityList(String packageName,
+            UserHandle user) {
+        Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName);
+        return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+                .stream()
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java
new file mode 100644
index 0000000..0e7c1de
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shadows;
+
+import android.graphics.Typeface;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowTypeface;
+
+/**
+ * Extension of {@link ShadowTypeface} with missing shadow methods
+ */
+@Implements(Typeface.class)
+public class LShadowTypeface extends ShadowTypeface {
+
+    @Implementation
+    public static Typeface create(Typeface family, int weight, boolean italic) {
+        int style = italic ? Typeface.ITALIC : Typeface.NORMAL;
+        if (weight >= 400) {
+            style |= Typeface.BOLD;
+        }
+        return create(family, style);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
new file mode 100644
index 0000000..edf8edb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.SparseBooleanArray;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowUserManager;
+
+/**
+ * Extension of {@link ShadowUserManager} with missing shadow methods
+ */
+@Implements(value = UserManager.class)
+public class LShadowUserManager extends ShadowUserManager {
+
+    private final SparseBooleanArray mQuietUsers = new SparseBooleanArray();
+    private final SparseBooleanArray mLockedUsers = new SparseBooleanArray();
+
+    @Implementation
+    protected boolean isQuietModeEnabled(UserHandle userHandle) {
+        return mQuietUsers.get(userHandle.hashCode());
+    }
+
+    public void setQuietModeEnabled(UserHandle userHandle, boolean enabled) {
+        mQuietUsers.put(userHandle.hashCode(), enabled);
+    }
+
+    @Implementation
+    protected boolean isUserUnlocked(UserHandle userHandle) {
+        return !mLockedUsers.get(userHandle.hashCode());
+    }
+
+    public void setUserLocked(UserHandle userHandle, boolean enabled) {
+        mLockedUsers.put(userHandle.hashCode(), enabled);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java
new file mode 100644
index 0000000..d60251c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowUserManager;
+import org.robolectric.shadows.ShadowWallpaperManager;
+
+/**
+ * Extension of {@link ShadowUserManager} with missing shadow methods
+ */
+@Implements(WallpaperManager.class)
+public class LShadowWallpaperManager extends ShadowWallpaperManager {
+
+    @Implementation
+    protected static WallpaperManager getInstance(Context context) {
+        return context.getSystemService(WallpaperManager.class);
+    }
+
+    /**
+     * Remove this once the fix for
+     * https://github.com/robolectric/robolectric/issues/5285
+     * is available
+     */
+    public static void initializeMock() {
+        WallpaperManager wm = mock(WallpaperManager.class);
+        ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
+        shadowApplication.setSystemService(Context.WALLPAPER_SERVICE, wm);
+        doReturn(0).when(wm).getDesiredMinimumWidth();
+        doReturn(0).when(wm).getDesiredMinimumHeight();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
new file mode 100644
index 0000000..344f532
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.content.Context;
+
+import com.android.launcher3.uioverrides.DeviceFlag;
+import com.android.launcher3.util.LooperExecutor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
+ */
+@Implements(value = DeviceFlag.class, isInAndroidSdk = false)
+public class ShadowDeviceFlag {
+
+    /**
+     * Mock change listener as it uses internal system classes not available to robolectric
+     */
+    @Implementation
+    protected void addChangeListener(Context context, Runnable r) { }
+
+    @Implementation
+    protected static boolean getDeviceValue(String key, boolean defaultValue) {
+        return defaultValue;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
new file mode 100644
index 0000000..a3b7dc7
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import static org.robolectric.shadow.api.Shadow.directlyOn;
+import static org.robolectric.util.ReflectionHelpers.setField;
+
+import android.os.Handler;
+
+import com.android.launcher3.util.LooperExecutor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+/**
+ * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
+ */
+@Implements(value = LooperExecutor.class, isInAndroidSdk = false)
+public class ShadowLooperExecutor {
+
+    @RealObject private LooperExecutor mRealExecutor;
+
+    @Implementation
+    protected Handler getHandler() {
+        Handler handler = directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
+        Thread thread = handler.getLooper().getThread();
+        if (!thread.isAlive()) {
+            // Robolectric destroys all loopers at the end of every test. Since Launcher maintains
+            // some static threads, they need to be reinitialized in case they were destroyed.
+            setField(mRealExecutor, "mHandler",
+                    new Handler(createAndStartNewLooper(thread.getName())));
+        }
+        return directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
new file mode 100644
index 0000000..6e2ccf8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Shadow for {@link MainThreadInitializedObject} to provide reset functionality for static sObjects
+ */
+@Implements(value = MainThreadInitializedObject.class, isInAndroidSdk = false)
+public class ShadowMainThreadInitializedObject {
+
+    // Keep reference to all created MainThreadInitializedObject so they can be cleared after test
+    private static Set<MainThreadInitializedObject> sObjects =
+            Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
+
+    @RealObject private MainThreadInitializedObject mRealObject;
+
+    @Implementation
+    protected void __constructor__(ObjectProvider provider) {
+        invokeConstructor(MainThreadInitializedObject.class, mRealObject,
+                from(ObjectProvider.class, provider));
+        sObjects.add(mRealObject);
+    }
+
+    /**
+     * Resets all the initialized sObjects to be null
+     */
+    public static void resetInitializedObjects() {
+        for (MainThreadInitializedObject object : new ArrayList<>(sObjects)) {
+            object.initializeForTesting(null);
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowOverrides.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowOverrides.java
new file mode 100644
index 0000000..131f691
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowOverrides.java
@@ -0,0 +1,61 @@
+/*
+ * 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.shadows;
+
+import android.content.Context;
+
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.ResourceBasedOverride.Overrides;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shadow for {@link Overrides} to provide custom overrides for test
+ */
+@Implements(value = Overrides.class, isInAndroidSdk = false)
+public class ShadowOverrides {
+
+    private static Map<Class, ObjectProvider> sProviderMap = new HashMap<>();
+
+    @Implementation
+    public static <T extends ResourceBasedOverride> T getObject(
+            Class<T> clazz, Context context, int resId) {
+        ObjectProvider<T> provider = sProviderMap.get(clazz);
+        if (provider != null) {
+            return provider.get(context);
+        }
+        return Shadow.directlyOn(Overrides.class, "getObject",
+                ClassParameter.from(Class.class, clazz),
+                ClassParameter.from(Context.class, context),
+                ClassParameter.from(int.class, resId));
+    }
+
+    public static <T> void setProvider(Class<T> clazz, ObjectProvider<T> provider) {
+        sProviderMap.put(clazz, provider);
+    }
+
+    public static void clearProvider() {
+        sProviderMap.clear();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowSurfaceTransactionApplier.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowSurfaceTransactionApplier.java
new file mode 100644
index 0000000..a9f2f27
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowSurfaceTransactionApplier.java
@@ -0,0 +1,42 @@
+/*
+ * 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.shadows;
+
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+
+import android.view.View;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+/**
+ * Shadow for SurfaceTransactionApplier to override default functionality
+ */
+@Implements(className = "com.android.quickstep.util.SurfaceTransactionApplier",
+        isInAndroidSdk = false)
+public class ShadowSurfaceTransactionApplier {
+
+    @RealObject
+    private Object mRealObject;
+
+    @Implementation
+    protected void __constructor__(View view) {
+        invokeConstructor(mRealObject.getClass(), mRealObject, from(View.class, null));
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
new file mode 100644
index 0000000..d330d10
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.ui;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.LauncherUIHelper.buildAndBindLauncher;
+import static com.android.launcher3.util.LauncherUIHelper.doLayout;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerProperties;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.folder.FolderPagedView;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.shadows.ShadowOverrides;
+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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowLooper;
+
+/**
+ * Tests scroll behavior at various Launcher UI components
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class LauncherUIScrollTest {
+
+    private Context mTargetContext;
+    private InvariantDeviceProfile mIdp;
+    private LauncherModelHelper mModelHelper;
+
+    private LauncherLayoutBuilder mLayoutBuilder;
+
+    @Before
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+        ShadowOverrides.setProvider(UserEventDispatcher.class,
+                c -> mock(UserEventDispatcher.class));
+
+        Settings.Global.putFloat(mTargetContext.getContentResolver(),
+                Settings.Global.WINDOW_ANIMATION_SCALE, 0);
+
+        mModelHelper.installApp(TEST_PACKAGE);
+        // LayoutBuilder with 3 workspace pages
+        mLayoutBuilder = new LauncherLayoutBuilder()
+                .atWorkspace(0,  mIdp.numRows - 1, 0).putApp(TEST_PACKAGE, TEST_PACKAGE)
+                .atWorkspace(0,  mIdp.numRows - 1, 1).putApp(TEST_PACKAGE, TEST_PACKAGE)
+                .atWorkspace(0,  mIdp.numRows - 1, 2).putApp(TEST_PACKAGE, TEST_PACKAGE);
+    }
+
+    @Test
+    public void testWorkspacePagesBound() throws Exception {
+        // Verify that the workspace if bound synchronously
+        Launcher launcher = loadLauncher();
+        assertEquals(3, launcher.getWorkspace().getPageCount());
+        assertEquals(0, launcher.getWorkspace().getCurrentPage());
+
+        launcher.dispatchGenericMotionEvent(createScrollEvent(-1));
+        assertNotEquals("Workspace was not scrolled",
+                0, launcher.getWorkspace().getNextPage());
+    }
+
+    @Test
+    public void testAllAppsScroll() throws Exception {
+        // Install 100 apps
+        for (int i = 0; i < 100; i++) {
+            mModelHelper.installApp(TEST_PACKAGE + i);
+        }
+
+        // Bind and open all-apps
+        Launcher launcher = loadLauncher();
+        launcher.getStateManager().goToState(LauncherState.ALL_APPS, false);
+        doLayout(launcher);
+
+        int currentScroll = launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
+        launcher.dispatchGenericMotionEvent(createScrollEvent(-1));
+        int newScroll = launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
+
+        assertNotEquals("All Apps was not scrolled", currentScroll, newScroll);
+        assertEquals("Workspace was scrolled", 0, launcher.getWorkspace().getNextPage());
+    }
+
+    @Test
+    public void testWidgetsListScroll() throws Exception {
+        // Install 100 widgets
+        for (int i = 0; i < 100; i++) {
+            mModelHelper.installCustomShortcut(TEST_PACKAGE + i, "shortcutProvider");
+        }
+
+        // Bind and open widgets
+        Launcher launcher = loadLauncher();
+        WidgetsFullSheet widgets = WidgetsFullSheet.show(launcher, false);
+        doLayout(launcher);
+
+        int currentScroll = widgets.getRecyclerView().getCurrentScrollY();
+        launcher.dispatchGenericMotionEvent(createScrollEvent(-1));
+        int newScroll = widgets.getRecyclerView().getCurrentScrollY();
+        assertNotEquals("Widgets was not scrolled", currentScroll, newScroll);
+        assertEquals("Workspace was scrolled", 0, launcher.getWorkspace().getNextPage());
+    }
+
+    @Test
+    public void testFolderPageScroll() throws Exception {
+        // Add a folder with multiple icons
+        FolderBuilder fb = mLayoutBuilder.atWorkspace(mIdp.numColumns / 2, mIdp.numRows / 2, 0)
+                .putFolder(0);
+        for (int i = 0; i < 100; i++) {
+            fb.addApp(TEST_PACKAGE, TEST_PACKAGE);
+        }
+
+        // Bind and open folder
+        Launcher launcher = loadLauncher();
+        doLayout(launcher);
+        launcher.getWorkspace().getFirstMatch((i, v) -> v instanceof FolderIcon).performClick();
+        ShadowLooper.idleMainLooper();
+        doLayout(launcher);
+        FolderPagedView folderPages = Folder.getOpen(launcher).getContent();
+
+        assertEquals(0, folderPages.getNextPage());
+        launcher.dispatchGenericMotionEvent(createScrollEvent(-1));
+        assertNotEquals("Folder page was not scrolled", 0, folderPages.getNextPage());
+        assertEquals("Workspace was scrolled", 0, launcher.getWorkspace().getNextPage());
+    }
+
+    private Launcher loadLauncher() throws Exception {
+        mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder).loadModelSync();
+        return buildAndBindLauncher();
+    }
+
+    private static MotionEvent createScrollEvent(int scroll) {
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE
+                .get(RuntimeEnvironment.application).portraitProfile;
+
+        final PointerProperties[] pointerProperties = new PointerProperties[1];
+        pointerProperties[0] = new PointerProperties();
+        pointerProperties[0].id = 0;
+        final MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
+        coords[0] = new MotionEvent.PointerCoords();
+        coords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, scroll);
+        coords[0].x = dp.widthPx / 2;
+        coords[0].y = dp.heightPx / 2;
+
+        final long time = SystemClock.uptimeMillis();
+        return MotionEvent.obtain(time, time, MotionEvent.ACTION_SCROLL, 1,
+                pointerProperties, coords, 0, 0, 1.0f, 1.0f, 0, 0,
+                InputDevice.SOURCE_CLASS_POINTER, 0);
+    }
+
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
index aa51ad2..2f3fc37 100644
--- a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
@@ -1,13 +1,13 @@
 package com.android.launcher3.util;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
 /**
  * Unit tests for {@link GridOccupancy}
  */
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
index 8513353..7a8c00b 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
@@ -17,15 +17,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import org.robolectric.RobolectricTestRunner;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
 /**
  * Robolectric unit tests for {@link IntSet}
  */
diff --git a/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
similarity index 95%
rename from tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
rename to robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
index d3659eb..4e21dce 100644
--- a/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
@@ -47,6 +47,7 @@
     private static final String ATTR_PACKAGE_NAME = "packageName";
     private static final String ATTR_CLASS_NAME = "className";
     private static final String ATTR_TITLE = "title";
+    private static final String ATTR_TITLE_TEXT = "titleText";
     private static final String ATTR_SCREEN = "screen";
 
     // x and y can be specified as negative integers, in which case -1 represents the
@@ -145,8 +146,17 @@
         }
 
         public FolderBuilder putFolder(int titleResId) {
-            FolderBuilder folderBuilder = new FolderBuilder();
             items.put(ATTR_TITLE, Integer.toString(titleResId));
+            return putFolder();
+        }
+
+        public FolderBuilder putFolder(String title) {
+            items.put(ATTR_TITLE_TEXT, title);
+            return putFolder();
+        }
+
+        private FolderBuilder putFolder() {
+            FolderBuilder folderBuilder = new FolderBuilder();
             items.put(ATTR_CHILDREN, folderBuilder.mChildren);
             mNodes.add(Pair.create(TAG_FOLDER, items));
             return folderBuilder;
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
new file mode 100644
index 0000000..0388087
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static android.content.Intent.ACTION_CREATE_SHORTCUT;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Process;
+import android.provider.Settings;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+
+import org.mockito.ArgumentCaptor;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * Utility class to help manage Launcher Model and related objects for test.
+ */
+public class LauncherModelHelper {
+
+    public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+    public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
+    public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+    public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+    public static final int NO__ICON = -1;
+    public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
+
+    // Authority for providing a dummy default-workspace-layout data.
+    private static final String TEST_PROVIDER_AUTHORITY =
+            LauncherModelHelper.class.getName().toLowerCase();
+    private static final int DEFAULT_BITMAP_SIZE = 10;
+    private static final int DEFAULT_GRID_SIZE = 4;
+
+    private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
+    public final TestLauncherProvider provider;
+    private final long mDefaultProfileId;
+
+    private BgDataModel mDataModel;
+    private AllAppsList mAllAppsList;
+
+    public LauncherModelHelper() {
+        provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
+        mDefaultProfileId = UserCache.INSTANCE.get(RuntimeEnvironment.application)
+                .getSerialNumberForUser(Process.myUserHandle());
+        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
+    }
+
+    public LauncherModel getModel() {
+        return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel();
+    }
+
+    public synchronized BgDataModel getBgDataModel() {
+        if (mDataModel == null) {
+            mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel");
+        }
+        return mDataModel;
+    }
+
+    public synchronized AllAppsList getAllAppsList() {
+        if (mAllAppsList == null) {
+            mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList");
+        }
+        return mAllAppsList;
+    }
+
+    /**
+     * Synchronously executes the task and returns all the UI callbacks posted.
+     */
+    public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
+        LauncherModel model = getModel();
+        if (!model.isModelLoaded()) {
+            ReflectionHelpers.setField(model, "mModelLoaded", true);
+        }
+        Executor mockExecutor = mock(Executor.class);
+        model.enqueueModelUpdateTask(new ModelUpdateTask() {
+            @Override
+            public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
+                    AllAppsList allAppsList, Executor uiExecutor) {
+                task.init(app, model, dataModel, allAppsList, mockExecutor);
+            }
+
+            @Override
+            public void run() {
+                task.run();
+            }
+        });
+        MODEL_EXECUTOR.submit(() -> null).get();
+
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mockExecutor, atLeast(0)).execute(captor.capture());
+        return captor.getAllValues();
+    }
+
+    /**
+     * Synchronously executes a task on the model
+     */
+    public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception {
+        BgDataModel dataModel = getBgDataModel();
+        return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get();
+    }
+
+    /**
+     * Initializes mock data for the test.
+     */
+    public void initializeData(String resourceName) throws Exception {
+        Context targetContext = RuntimeEnvironment.application;
+        BgDataModel bgDataModel = getBgDataModel();
+        AllAppsList allAppsList = getAllAppsList();
+
+        MODEL_EXECUTOR.submit(() -> {
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                    this.getClass().getResourceAsStream(resourceName)))) {
+                String line;
+                HashMap<String, Class> classMap = new HashMap<>();
+                while ((line = reader.readLine()) != null) {
+                    line = line.trim();
+                    if (line.startsWith("#") || line.isEmpty()) {
+                        continue;
+                    }
+                    String[] commands = line.split(" ");
+                    switch (commands[0]) {
+                        case "classMap":
+                            classMap.put(commands[1], Class.forName(commands[2]));
+                            break;
+                        case "bgItem":
+                            bgDataModel.addItem(targetContext,
+                                    (ItemInfo) initItem(classMap.get(commands[1]), commands, 2),
+                                    false);
+                            break;
+                        case "allApps":
+                            allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
+                            break;
+                    }
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }).get();
+    }
+
+    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
+        HashMap<String, Field> cache = mFieldCache.get(clazz);
+        if (cache == null) {
+            cache = new HashMap<>();
+            Class c = clazz;
+            while (c != null) {
+                for (Field f : c.getDeclaredFields()) {
+                    f.setAccessible(true);
+                    cache.put(f.getName(), f);
+                }
+                c = c.getSuperclass();
+            }
+            mFieldCache.put(clazz, cache);
+        }
+
+        Object item = clazz.newInstance();
+        for (int i = startIndex; i < fieldDef.length; i++) {
+            String[] fieldData = fieldDef[i].split("=", 2);
+            Field f = cache.get(fieldData[0]);
+            Class type = f.getType();
+            if (type == int.class || type == long.class) {
+                f.set(item, Integer.parseInt(fieldData[1]));
+            } else if (type == CharSequence.class || type == String.class) {
+                f.set(item, fieldData[1]);
+            } else if (type == Intent.class) {
+                if (!fieldData[1].startsWith("#Intent")) {
+                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
+                }
+                f.set(item, Intent.parseUri(fieldData[1], 0));
+            } else if (type == ComponentName.class) {
+                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
+            } else {
+                throw new Exception("Added parsing logic for "
+                        + f.getName() + " of type " + f.getType());
+            }
+        }
+        return item;
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y) {
+        return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, long profileId) {
+        return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, String packageName) {
+        return addItem(type, screen, container, x, y, mDefaultProfileId, packageName);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, String packageName,
+            int id, Uri contentUri) {
+        addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri);
+        return id;
+    }
+
+    /**
+     * Adds a dummy item in the DB.
+     * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
+     *             folder (where the type represents the number of items in the folder).
+     */
+    public int addItem(int type, int screen, int container, int x, int y, long profileId,
+            String packageName) {
+        Context context = RuntimeEnvironment.application;
+        int id = LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI);
+        return id;
+    }
+
+    public void addItem(int type, int screen, int container, int x, int y, long profileId,
+            String packageName, int id, Uri contentUri) {
+        Context context = RuntimeEnvironment.application;
+
+        ContentValues values = new ContentValues();
+        values.put(LauncherSettings.Favorites._ID, id);
+        values.put(LauncherSettings.Favorites.CONTAINER, container);
+        values.put(LauncherSettings.Favorites.SCREEN, screen);
+        values.put(LauncherSettings.Favorites.CELLX, x);
+        values.put(LauncherSettings.Favorites.CELLY, y);
+        values.put(LauncherSettings.Favorites.SPANX, 1);
+        values.put(LauncherSettings.Favorites.SPANY, 1);
+        values.put(LauncherSettings.Favorites.PROFILE_ID, profileId);
+
+        if (type == APP_ICON || type == SHORTCUT) {
+            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
+            values.put(LauncherSettings.Favorites.INTENT,
+                    new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
+        } else {
+            values.put(LauncherSettings.Favorites.ITEM_TYPE,
+                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
+            // Add folder items.
+            for (int i = 0; i < type; i++) {
+                addItem(APP_ICON, 0, id, 0, 0, profileId);
+            }
+        }
+
+        context.getContentResolver().insert(contentUri, values);
+    }
+
+    public int[][][] createGrid(int[][][] typeArray) {
+        return createGrid(typeArray, 1);
+    }
+
+    public int[][][] createGrid(int[][][] typeArray, int startScreen) {
+        final Context context = RuntimeEnvironment.application;
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+        return createGrid(typeArray, startScreen, mDefaultProfileId);
+    }
+
+    /**
+     * Initializes the DB with dummy elements to represent the provided grid structure.
+     * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
+     *                  type definitions. The first dimension represents the screens and the next
+     *                  two represent the workspace grid.
+     * @param startScreen First screen id from where the icons will be added.
+     * @return the same grid representation where each entry is the corresponding item id.
+     */
+    public int[][][] createGrid(int[][][] typeArray, int startScreen, long profileId) {
+        Context context = RuntimeEnvironment.application;
+        int[][][] ids = new int[typeArray.length][][];
+        for (int i = 0; i < typeArray.length; i++) {
+            // Add screen to DB
+            int screenId = startScreen + i;
+
+            // Keep the screen id counter up to date
+            LauncherSettings.Settings.call(context.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+            ids[i] = new int[typeArray[i].length][];
+            for (int y = 0; y < typeArray[i].length; y++) {
+                ids[i][y] = new int[typeArray[i][y].length];
+                for (int x = 0; x < typeArray[i][y].length; x++) {
+                    if (typeArray[i][y][x] < 0) {
+                        // Empty cell
+                        ids[i][y][x] = -1;
+                    } else {
+                        ids[i][y][x] = addItem(
+                                typeArray[i][y][x], screenId, DESKTOP, x, y, profileId);
+                    }
+                }
+            }
+        }
+
+        return ids;
+    }
+
+    /**
+     * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+     */
+    public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
+            throws Exception {
+        Context context = RuntimeEnvironment.application;
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
+        idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+        idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+
+        Settings.Secure.putString(context.getContentResolver(),
+                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+        shadowOf(context.getPackageManager())
+                .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+                TEST_PROVIDER_AUTHORITY;
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        builder.build(new OutputStreamWriter(bos));
+        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
+        shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
+                new ByteArrayInputStream(bos.toByteArray()));
+        return this;
+    }
+
+    /**
+     * Simulates an apk install with a default main activity with same class and package name
+     */
+    public void installApp(String component) throws NameNotFoundException {
+        IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
+        filter.addCategory(Intent.CATEGORY_LAUNCHER);
+        installApp(component, component, filter);
+    }
+
+    /**
+     * Simulates a custom shortcut install
+     */
+    public void installCustomShortcut(String pkg, String clazz) throws NameNotFoundException {
+        installApp(pkg, clazz, new IntentFilter(ACTION_CREATE_SHORTCUT));
+    }
+
+    private void installApp(String pkg, String clazz, IntentFilter filter)
+            throws NameNotFoundException {
+        ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+        ComponentName cn = new ComponentName(pkg, clazz);
+        spm.addActivityIfNotPresent(cn);
+
+        filter.addCategory(Intent.CATEGORY_DEFAULT);
+        spm.addIntentFilterForActivity(cn, filter);
+    }
+
+    /**
+     * Loads the model in memory synchronously
+     */
+    public void loadModelSync() throws ExecutionException, InterruptedException {
+        // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+        // so that we can wait appropriately for the loader to complete.
+        ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.UI_HELPER_EXECUTOR);
+
+        Callbacks mockCb = mock(Callbacks.class);
+        getModel().addCallbacksAndLoad(mockCb);
+
+        Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get();
+        ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.MAIN_EXECUTOR);
+        getModel().removeCallbacks(mockCb);
+    }
+
+    /**
+     * An extension of LauncherProvider backed up by in-memory database.
+     */
+    public static class TestLauncherProvider extends LauncherProvider {
+
+        @Override
+        public boolean onCreate() {
+            return true;
+        }
+
+        public SQLiteDatabase getDb() {
+            createDbIfNotExists();
+            return mOpenHelper.getWritableDatabase();
+        }
+
+        public DatabaseHelper getHelper() {
+            return mOpenHelper;
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java b/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java
new file mode 100644
index 0000000..6dd4df8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.Application;
+
+import com.android.launcher3.shadows.LShadowWallpaperManager;
+import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
+import com.android.launcher3.shadows.ShadowOverrides;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+
+import org.robolectric.TestLifecycleApplication;
+import org.robolectric.shadows.ShadowLog;
+
+import java.lang.reflect.Method;
+
+public class LauncherTestApplication extends Application implements TestLifecycleApplication {
+
+    @Override
+    public void beforeTest(Method method) {
+        ShadowLog.stream = System.out;
+
+        // Disable plugins
+        PluginManagerWrapper.INSTANCE.initializeForTesting(mock(PluginManagerWrapper.class));
+
+        // Initialize mock wallpaper manager
+        LShadowWallpaperManager.initializeMock();
+    }
+
+    @Override
+    public void prepareTest(Object test) { }
+
+    @Override
+    public void afterTest(Method method) {
+        ShadowLog.stream = null;
+        ShadowMainThreadInitializedObject.resetInitializedObjects();
+        ShadowOverrides.clearProvider();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
new file mode 100644
index 0000000..f019a20
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.Point;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.launcher3.Launcher;
+
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.List;
+
+/**
+ * Utility class to help manage Launcher UI and related objects for test.
+ */
+public class LauncherUIHelper {
+
+    /**
+     * Returns the class name for the Launcher activity as defined in the manifest
+     */
+    public static String getLauncherClassName() {
+        Context context = RuntimeEnvironment.application;
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(context.getPackageName())
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        List<ResolveInfo> launchers = context.getPackageManager()
+                .queryIntentActivities(homeIntent, 0);
+        if (launchers.size() != 1) {
+            return null;
+        }
+        return launchers.get(0).activityInfo.name;
+    }
+
+    /**
+     * Returns an activity controller for Launcher activity defined in the manifest
+     */
+    public static <T extends Launcher> ActivityController<T> buildLauncher() {
+        try {
+            Class<T> tClass = (Class<T>) Class.forName(getLauncherClassName());
+            return Robolectric.buildActivity(tClass);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Creates and binds a Launcher activity defined in the manifest.
+     * Note that the model must be bound before calling this
+     */
+    public static <T extends Launcher> T buildAndBindLauncher() {
+        ActivityController<T> controller = buildLauncher();
+
+        T launcher = controller.setup().get();
+        doLayout(launcher);
+        ViewOnDrawExecutor executor = ReflectionHelpers.getField(launcher, "mPendingExecutor");
+        if (executor != null) {
+            executor.runAllTasks();
+        }
+        return launcher;
+    }
+
+    /**
+     * Performs a measure and layout pass for the given activity
+     */
+    public static void doLayout(Activity activity) {
+        Point size = new Point();
+        RuntimeEnvironment.application.getSystemService(WindowManager.class)
+                .getDefaultDisplay().getSize(size);
+        View view = activity.getWindow().getDecorView();
+        view.measure(makeMeasureSpec(size.x, EXACTLY), makeMeasureSpec(size.y, EXACTLY));
+        view.layout(0, 0, size.x, size.y);
+        ShadowLooper.idleMainLooper();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
deleted file mode 100644
index 31e303e..0000000
--- a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.android.launcher3.util;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-
-import com.android.launcher3.LauncherProvider;
-
-/**
- * An extension of LauncherProvider backed up by in-memory database.
- */
-public class TestLauncherProvider extends LauncherProvider {
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    protected synchronized void createDbIfNotExists() {
-        if (mOpenHelper == null) {
-            mOpenHelper = new MyDatabaseHelper(getContext());
-        }
-    }
-
-    public SQLiteDatabase getDb() {
-        createDbIfNotExists();
-        return mOpenHelper.getWritableDatabase();
-    }
-
-    @Override
-    protected void notifyListeners() { }
-
-    private static class MyDatabaseHelper extends DatabaseHelper {
-        public MyDatabaseHelper(Context context) {
-            super(context, null, null);
-            initIds();
-        }
-
-        @Override
-        public long getDefaultUserSerial() {
-            return 0;
-        }
-
-        @Override
-        protected void onEmptyDbCreated() { }
-
-        @Override
-        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
-    }
-}
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
similarity index 80%
rename from tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
rename to robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index a31d8a6..84c65b1 100644
--- a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -19,37 +19,38 @@
 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 androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 import android.view.LayoutInflater;
 
-import com.android.launcher3.icons.IconCache;
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.MultiHashMap;
+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.Map;
+import java.util.Collections;
 
-import androidx.recyclerview.widget.RecyclerView;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
 public class WidgetsListAdapterTest {
 
     @Mock private LayoutInflater mMockLayoutInflater;
@@ -64,7 +65,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mContext = InstrumentationRegistry.getTargetContext();
+        mContext = RuntimeEnvironment.application;
         mTestProfile = new InvariantDeviceProfile();
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
@@ -121,29 +122,28 @@
     /**
      * Helper method to generate the sample widget model map that can be used for the tests
      * @param num the number of WidgetItem the map should contain
-     * @return
      */
     private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
         ArrayList<WidgetListRowEntry> result = new ArrayList<>();
         if (num <= 0) return result;
+        ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
 
-        MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
-        AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext);
-        for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
+        for (int i = 0; i < num; i++) {
+            ComponentName cn = new ComponentName("com.dummy.apk" + i, "DummyWidet");
+
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
+
             WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
                     .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
 
             PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
             pInfo.title = pInfo.packageName;
             pInfo.user = wi.user;
-            pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
-            newMap.addToList(pInfo, wi);
-            if (newMap.size() == num) {
-                break;
-            }
-        }
-        for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : newMap.entrySet()) {
-            result.add(new WidgetListRowEntry(entry.getKey(), entry.getValue()));
+            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/settings.gradle b/settings.gradle
index b52bd4f..ce13bfb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,5 @@
 include ':IconLoader'
 project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib')
+
+include ':SharedLibWrapper'
+project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index af2cdc3..cd27a2d 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 
-import android.animation.Animator;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.util.AttributeSet;
@@ -30,11 +29,12 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
 import android.widget.LinearLayout;
 
 import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
 
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TouchController;
@@ -59,6 +59,7 @@
             TYPE_DISCOVERY_BOUNCE,
             TYPE_SNACKBAR,
             TYPE_LISTENER,
+            TYPE_ALL_APPS_EDU,
 
             TYPE_TASK_MENU,
             TYPE_OPTIONS_POPUP
@@ -74,25 +75,28 @@
     public static final int TYPE_DISCOVERY_BOUNCE = 1 << 6;
     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;
 
     // Popups related to quickstep UI
-    public static final int TYPE_TASK_MENU = 1 << 9;
-    public static final int TYPE_OPTIONS_POPUP = 1 << 10;
+    public static final int TYPE_TASK_MENU = 1 << 10;
+    public static final int TYPE_OPTIONS_POPUP = 1 << 11;
 
     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_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
-            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE;
+            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
+            | TYPE_ALL_APPS_EDU;
 
     // Usually we show the back button when a floating view is open. Instead, hide for these types.
     public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
             | TYPE_SNACKBAR | TYPE_WIDGET_RESIZE_FRAME | TYPE_LISTENER;
 
-    public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER;
+    public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
+            & ~TYPE_ALL_APPS_EDU;
 
     // 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 |
@@ -134,9 +138,8 @@
      * Creates a user-controlled animation to hint that the view will be closed if completed.
      * @param distanceToMove The max distance that elements should move from their starting point.
      */
-    public @Nullable Animator createHintCloseAnim(float distanceToMove) {
-        return null;
-    }
+    public void addHintCloseAnim(
+            float distanceToMove, Interpolator interpolator, PendingAnimation target) { }
 
     public abstract void logActionCommand(int command);
 
@@ -171,7 +174,8 @@
                 targetInfo.first, TYPE_WINDOW_STATE_CHANGED, targetInfo.second);
 
         if (mIsOpen) {
-            performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+            getAccessibilityInitialFocusView().performAccessibilityAction(
+                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
         }
         ActivityContext.lookupContext(getContext()).getDragLayer()
                 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
@@ -181,7 +185,15 @@
         return null;
     }
 
-    protected static <T extends AbstractFloatingView> T getOpenView(
+    /** Returns the View that Accessibility services should focus on first. */
+    protected View getAccessibilityInitialFocusView() {
+        return this;
+    }
+
+    /**
+     * Returns a view matching FloatingViewType
+     */
+    public static <T extends AbstractFloatingView> T getOpenView(
             ActivityContext activity, @FloatingViewType int type) {
         BaseDragLayer dragLayer = activity.getDragLayer();
         if (dragLayer == null) return null;
@@ -251,4 +263,8 @@
             @FloatingViewType int type) {
         return getOpenView(activity, type);
     }
+
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index f9a8d1b..6f7b684 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -22,6 +22,7 @@
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.util.FocusLogic;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 import java.util.ArrayList;
@@ -35,7 +36,12 @@
     private static final Rect sTmpRect = new Rect();
 
     // Represents the cell size on the grid in the two orientations.
-    private static Point[] sCellSize;
+    private static final MainThreadInitializedObject<Point[]> CELL_SIZE =
+            new MainThreadInitializedObject<>(c -> {
+                InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
+                return new Point[] {inv.landscapeProfile.getCellSize(),
+                        inv.portraitProfile.getCellSize()};
+            });
 
     private static final int HANDLE_COUNT = 4;
     private static final int INDEX_LEFT = 0;
@@ -344,7 +350,7 @@
         mWidgetView.requestLayout();
     }
 
-    static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
+    public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
             int spanX, int spanY) {
         getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
         widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
@@ -352,27 +358,19 @@
     }
 
     public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
-        if (sCellSize == null) {
-            InvariantDeviceProfile inv = LauncherAppState.getIDP(context);
-
-            // Initiate cell sizes.
-            sCellSize = new Point[2];
-            sCellSize[0] = inv.landscapeProfile.getCellSize();
-            sCellSize[1] = inv.portraitProfile.getCellSize();
-        }
-
         if (rect == null) {
             rect = new Rect();
         }
         final float density = context.getResources().getDisplayMetrics().density;
+        final Point[] cellSize = CELL_SIZE.get(context);
 
         // Compute landscape size
-        int landWidth = (int) ((spanX * sCellSize[0].x) / density);
-        int landHeight = (int) ((spanY * sCellSize[0].y) / density);
+        int landWidth = (int) ((spanX * cellSize[0].x) / density);
+        int landHeight = (int) ((spanY * cellSize[0].y) / density);
 
         // Compute portrait size
-        int portWidth = (int) ((spanX * sCellSize[1].x) / density);
-        int portHeight = (int) ((spanY * sCellSize[1].y) / density);
+        int portWidth = (int) ((spanX * cellSize[1].x) / density);
+        int portHeight = (int) ((spanY * cellSize[1].y) / density);
         rect.set(portWidth, landHeight, landWidth, portHeight);
         return rect;
     }
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index e3ef5d6..a55c90d 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -1,5 +1,7 @@
 package com.android.launcher3;
 
+import static android.os.Process.myUserHandle;
+
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -10,16 +12,15 @@
 import android.database.Cursor;
 import android.util.Log;
 
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.LoaderTask;
-import com.android.launcher3.provider.RestoreDbTask;
-import com.android.launcher3.util.ContentWriter;
-
 import androidx.annotation.WorkerThread;
 
-import static android.os.Process.myUserHandle;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.util.ContentWriter;
 
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
@@ -50,7 +51,7 @@
     @WorkerThread
     public static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
         AppWidgetHost appWidgetHost = new LauncherAppWidgetHost(context);
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             Log.e(TAG, "Skipping widget ID remap as widgets not supported");
             appWidgetHost.deleteHost();
             return;
@@ -82,7 +83,7 @@
 
             // b/135926478: Work profile widget restore is broken in platform. This forces us to
             // recreate the widget during loading with the correct host provider.
-            long mainProfileId = UserManagerCompat.getInstance(context)
+            long mainProfileId = UserCache.INSTANCE.get(context)
                     .getSerialNumberForUser(myUserHandle());
             String oldWidgetId = Integer.toString(oldWidgetIds[i]);
             int result = new ContentWriter(context, new ContentWriter.CommitParams(
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index ac43967..432073e 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -44,6 +44,8 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -130,6 +132,7 @@
     private static final String ATTR_PACKAGE_NAME = "packageName";
     private static final String ATTR_CLASS_NAME = "className";
     private static final String ATTR_TITLE = "title";
+    private static final String ATTR_TITLE_TEXT = "titleText";
     private static final String ATTR_SCREEN = "screen";
 
     // x and y can be specified as negative integers, in which case -1 represents the
@@ -583,7 +586,8 @@
             if (titleResId != 0) {
                 title = mSourceRes.getString(titleResId);
             } else {
-                title = "";
+                String titleText = getAttributeValue(parser, ATTR_TITLE_TEXT);
+                title = TextUtils.isEmpty(titleText) ? "" : titleText;
             }
 
             mValues.put(Favorites.TITLE, title);
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index b28077f..310c306 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -24,30 +25,34 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
 import android.view.ContextThemeWrapper;
 
 import androidx.annotation.IntDef;
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.logging.UserEventDispatcher.UserEventDelegate;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.views.ActivityContext;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.util.ArrayList;
 
-public abstract class BaseActivity extends Activity
-        implements UserEventDelegate, LogStateProvider, ActivityContext {
+/**
+ * Launcher BaseActivity
+ */
+public abstract class BaseActivity extends Activity implements ActivityContext {
+
+    private static final String TAG = "BaseActivity";
 
     public static final int INVISIBLE_BY_STATE_HANDLER = 1 << 0;
     public static final int INVISIBLE_BY_APP_TRANSITIONS = 1 << 1;
@@ -81,19 +86,44 @@
     protected StatsLogManager mStatsLogManager;
     protected SystemUiController mSystemUiController;
 
-    private static final int ACTIVITY_STATE_STARTED = 1 << 0;
-    private static final int ACTIVITY_STATE_RESUMED = 1 << 1;
+
+    public static final int ACTIVITY_STATE_STARTED = 1 << 0;
+    public static final int ACTIVITY_STATE_RESUMED = 1 << 1;
+
     /**
-     * State flag indicating if the user is active or the actitvity when to background as a result
+     * State flags indicating that the activity has received one frame after resume, and was
+     * not immediately paused.
+     */
+    public static final int ACTIVITY_STATE_DEFERRED_RESUMED = 1 << 2;
+
+    public static final int ACTIVITY_STATE_WINDOW_FOCUSED = 1 << 3;
+
+    /**
+     * State flag indicating if the user is active or the activity when to background as a result
      * of user action.
      * @see #isUserActive()
      */
-    private static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 2;
+    public static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 4;
+
+    /**
+     * State flag indicating if the user will be active shortly.
+     */
+    public static final int ACTIVITY_STATE_USER_WILL_BE_ACTIVE = 1 << 5;
+
+    /**
+     * State flag indicating that a state transition is in progress
+     */
+    public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 6;
 
     @Retention(SOURCE)
     @IntDef(
             flag = true,
-            value = {ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE})
+            value = {ACTIVITY_STATE_STARTED,
+                    ACTIVITY_STATE_RESUMED,
+                    ACTIVITY_STATE_DEFERRED_RESUMED,
+                    ACTIVITY_STATE_WINDOW_FOCUSED,
+                    ACTIVITY_STATE_USER_ACTIVE,
+                    ACTIVITY_STATE_TRANSITION_ACTIVE})
     public @interface ActivityFlags{}
 
     @ActivityFlags
@@ -114,20 +144,18 @@
         return mDeviceProfile;
     }
 
-    public int getCurrentState() { return StatsLogUtils.LAUNCHER_STATE_BACKGROUND; }
-
     public void modifyUserEvent(LauncherLogProto.LauncherEvent event) {}
 
     public final StatsLogManager getStatsLogManager() {
         if (mStatsLogManager == null) {
-            mStatsLogManager = StatsLogManager.newInstance(this, this);
+            mStatsLogManager = StatsLogManager.newInstance(this);
         }
         return mStatsLogManager;
     }
 
     public final UserEventDispatcher getUserEventDispatcher() {
         if (mUserEventDispatcher == null) {
-            mUserEventDispatcher = UserEventDispatcher.newInstance(this, this);
+            mUserEventDispatcher = UserEventDispatcher.newInstance(this);
         }
         return mUserEventDispatcher;
     }
@@ -146,19 +174,20 @@
 
     @Override
     protected void onStart() {
-        mActivityFlags |= ACTIVITY_STATE_STARTED;
+        addActivityFlags(ACTIVITY_STATE_STARTED);
         super.onStart();
     }
 
     @Override
     protected void onResume() {
-        mActivityFlags |= ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE;
+        addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
+        removeActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
         super.onResume();
     }
 
     @Override
     protected void onUserLeaveHint() {
-        mActivityFlags &= ~ACTIVITY_STATE_USER_ACTIVE;
+        removeActivityFlags(ACTIVITY_STATE_USER_ACTIVE);
         super.onUserLeaveHint();
     }
 
@@ -172,7 +201,7 @@
 
     @Override
     protected void onStop() {
-        mActivityFlags &= ~ACTIVITY_STATE_STARTED & ~ACTIVITY_STATE_USER_ACTIVE;
+        removeActivityFlags(ACTIVITY_STATE_STARTED | ACTIVITY_STATE_USER_ACTIVE);
         mForceInvisible = 0;
         super.onStop();
 
@@ -183,7 +212,7 @@
 
     @Override
     protected void onPause() {
-        mActivityFlags &= ~ACTIVITY_STATE_RESUMED;
+        removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED);
         super.onPause();
 
         // Reset the overridden sysui flags used for the task-swipe launch animation, we do this
@@ -193,6 +222,17 @@
         getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
     }
 
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            addActivityFlags(ACTIVITY_STATE_WINDOW_FOCUSED);
+        } else {
+            removeActivityFlags(ACTIVITY_STATE_WINDOW_FOCUSED);
+        }
+
+    }
+
     public boolean isStarted() {
         return (mActivityFlags & ACTIVITY_STATE_STARTED) != 0;
     }
@@ -208,6 +248,22 @@
         return (mActivityFlags & ACTIVITY_STATE_USER_ACTIVE) != 0;
     }
 
+    public int getActivityFlags() {
+        return mActivityFlags;
+    }
+
+    protected void addActivityFlags(int flags) {
+        mActivityFlags |= flags;
+        onActivityFlagsChanged(flags);
+    }
+
+    protected void removeActivityFlags(int flags) {
+        mActivityFlags &= ~flags;
+        onActivityFlagsChanged(flags);
+    }
+
+    protected void onActivityFlagsChanged(int changeBits) { }
+
     public void addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) {
         mDPChangeListeners.add(listener);
     }
@@ -233,7 +289,7 @@
     /**
      * Used to set the override visibility state, used only to handle the transition home with the
      * recents animation.
-     * @see QuickstepAppTransitionManagerImpl#getWallpaperOpenRunner()
+     * @see QuickstepAppTransitionManagerImpl#createWallpaperOpenRunner
      */
     public void addForceInvisibleFlag(@InvisibilityFlags int flag) {
         mForceInvisible |= flag;
@@ -258,13 +314,6 @@
         void onMultiWindowModeChanged(boolean isInMultiWindowMode);
     }
 
-    @Override
-    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
-        if (!UiFactory.dumpActivity(this, writer)) {
-            super.dump(prefix, fd, writer, args);
-        }
-    }
-
     protected void dumpMisc(String prefix, PrintWriter writer) {
         writer.println(prefix + "deviceProfile isTransposed="
                 + getDeviceProfile().isVerticalBarLayout());
@@ -274,6 +323,22 @@
         writer.println(prefix + "mForceInvisible: " + mForceInvisible);
     }
 
+    /**
+     * A wrapper around the platform method with Launcher specific checks
+     */
+    public void startShortcut(String packageName, String id, Rect sourceBounds,
+            Bundle startActivityOptions, UserHandle user) {
+        if (GO_DISABLE_WIDGETS) {
+            return;
+        }
+        try {
+            getSystemService(LauncherApps.class).startShortcut(packageName, id, sourceBounds,
+                    startActivityOptions, user);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to start shortcut", e);
+        }
+    }
+
     public static <T extends BaseActivity> T fromContext(Context context) {
         if (context instanceof BaseActivity) {
             return (T) context;
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 994ba65..9cb8cf2 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -16,10 +16,16 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_ROTATION;
+
 import android.app.ActivityOptions;
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Process;
@@ -27,25 +33,36 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.ActionMode;
+import android.view.Display;
 import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowInsets.Type;
+import android.view.WindowMetrics;
 import android.widget.Toast;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.uioverrides.DisplayRotationListener;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DefaultDisplay.DisplayInfoChangeListener;
+import com.android.launcher3.util.DefaultDisplay.Info;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.WindowBounds;
 
 /**
  * Extension of BaseActivity allowing support for drag-n-drop
  */
 public abstract class BaseDraggingActivity extends BaseActivity
-        implements WallpaperColorInfo.OnChangeListener {
+        implements WallpaperColorInfo.OnChangeListener, DisplayInfoChangeListener {
 
     private static final String TAG = "BaseDraggingActivity";
 
@@ -56,21 +73,21 @@
     private ActionMode mCurrentActionMode;
     protected boolean mIsSafeModeEnabled;
 
-    private OnStartCallback mOnStartCallback;
+    private Runnable mOnStartCallback;
 
     private int mThemeRes = R.style.AppTheme;
 
-    private DisplayRotationListener mRotationListener;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mIsSafeModeEnabled = getPackageManager().isSafeMode();
-        mRotationListener = new DisplayRotationListener(this, this::onDeviceRotationChanged);
+
+
+        mIsSafeModeEnabled = TraceHelper.whitelistIpcs("isSafeMode",
+                () -> getPackageManager().isSafeMode());
+        DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
 
         // Update theme
-        WallpaperColorInfo wallpaperColorInfo = WallpaperColorInfo.getInstance(this);
-        wallpaperColorInfo.addOnChangeListener(this);
+        WallpaperColorInfo.INSTANCE.get(this).addOnChangeListener(this);
         int themeRes = Themes.getActivityThemeRes(this);
         if (themeRes != mThemeRes) {
             mThemeRes = themeRes;
@@ -166,13 +183,16 @@
                 AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(),
                         Process.myUserHandle(), sourceContainer);
             } else {
-                LauncherAppsCompat.getInstance(this).startActivityForProfile(
+                getSystemService(LauncherApps.class).startMainActivity(
                         intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
                 AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(), user,
                         sourceContainer);
             }
-            getUserEventDispatcher().logAppLaunch(v, intent);
-            getStatsLogManager().logAppLaunch(v, intent);
+            getUserEventDispatcher().logAppLaunch(v, intent, user);
+            if (item != null) {
+                InstanceId instanceId = new InstanceIdSequence().newInstanceId();
+                logAppLaunch(item, instanceId);
+            }
             return true;
         } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
@@ -181,6 +201,11 @@
         return false;
     }
 
+    protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
+        getStatsLogManager().logger().withItemInfo(info).withInstanceId(instanceId)
+                .log(LAUNCHER_APP_LAUNCH_TAP);
+    }
+
     private void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info,
             @Nullable String sourceContainer) {
         try {
@@ -195,8 +220,7 @@
                 if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                     String id = ((WorkspaceItemInfo) info).getDeepShortcutId();
                     String packageName = intent.getPackage();
-                    DeepShortcutManager.getInstance(this).startShortcut(
-                            packageName, id, intent.getSourceBounds(), optsBundle, info.user);
+                    startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user);
                     AppLaunchTracker.INSTANCE.get(this).onStartShortcut(packageName, id, info.user,
                             sourceContainer);
                 } else {
@@ -222,7 +246,7 @@
         super.onStart();
 
         if (mOnStartCallback != null) {
-            mOnStartCallback.onActivityStart(this);
+            mOnStartCallback.run();
             mOnStartCallback = null;
         }
     }
@@ -230,36 +254,50 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        WallpaperColorInfo.getInstance(this).removeOnChangeListener(this);
-        mRotationListener.disable();
+        WallpaperColorInfo.INSTANCE.get(this).removeOnChangeListener(this);
+        DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
     }
 
-    public <T extends BaseDraggingActivity> void setOnStartCallback(OnStartCallback<T> callback) {
-        mOnStartCallback = callback;
+    public void runOnceOnStart(Runnable action) {
+        mOnStartCallback = action;
+    }
+
+    public void clearRunOnceOnStartCallback() {
+        mOnStartCallback = null;
     }
 
     protected void onDeviceProfileInitiated() {
         if (mDeviceProfile.isVerticalBarLayout()) {
-            mRotationListener.enable();
             mDeviceProfile.updateIsSeascape(this);
-        } else {
-            mRotationListener.disable();
         }
     }
 
-    private void onDeviceRotationChanged() {
-        if (mDeviceProfile.updateIsSeascape(this)) {
+    @Override
+    public void onDisplayInfoChanged(Info info, int flags) {
+        if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.updateIsSeascape(this)) {
             reapplyUi();
         }
     }
 
+    public OnClickListener getItemOnClickListener() {
+        return ItemClickHandler.INSTANCE;
+    }
+
     protected abstract void reapplyUi();
 
-    /**
-     * Callback for listening for onStart
-     */
-    public interface OnStartCallback<T extends BaseDraggingActivity> {
+    protected WindowBounds getMultiWindowDisplaySize() {
+        if (Utilities.ATLEAST_R) {
+            WindowMetrics wm = getWindowManager().getCurrentWindowMetrics();
 
-        void onActivityStart(T activity);
+            Insets insets = wm.getWindowInsets().getInsets(Type.systemBars());
+            return new WindowBounds(wm.getBounds(),
+                    new Rect(insets.left, insets.top, insets.right, insets.bottom));
+        }
+        // Note: Calls to getSize() can't rely on our cached DefaultDisplay since it can return
+        // the app window size
+        Display display = getWindowManager().getDefaultDisplay();
+        Point mwSize = new Point();
+        display.getSize(mwSize);
+        return new WindowBounds(new Rect(0, 0, mwSize.x, mwSize.y), new Rect());
     }
 }
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 864fa6e..8eceec0 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -16,17 +16,23 @@
 
 package com.android.launcher3;
 
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.views.RecyclerViewFastScroller;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.RecyclerViewFastScroller;
+
 
 /**
  * A base {@link RecyclerView}, which does the following:
@@ -138,7 +144,7 @@
         if (getCurrentScrollY() == 0) {
             return true;
         }
-        return false;
+        return getAdapter() == null || getAdapter().getItemCount() == 0;
     }
 
     /**
@@ -181,4 +187,20 @@
             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
         }
     }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (isLayoutSuppressed()) info.setScrollable(false);
+    }
+
+    @Override
+    public void setLayoutFrozen(boolean frozen) {
+        final boolean changing = frozen != isLayoutSuppressed();
+        super.setLayoutFrozen(frozen);
+        if (changing) {
+            ActivityContext.lookupContext(getContext()).getDragLayer()
+                    .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7adb6a4..60b6da6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.Animator;
@@ -27,6 +29,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -37,22 +40,27 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.ViewDebug;
 import android.widget.TextView;
 
 import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.IconShape;
 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.model.PackageItemInfo;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.model.data.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.IconLabelDotView;
 
@@ -64,7 +72,7 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
-        IconLabelDotView {
+        IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
@@ -72,6 +80,10 @@
 
     private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
 
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+
+    private float mScaleForReorderBounce = 1f;
 
     private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
             = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
@@ -102,13 +114,11 @@
 
     private final ActivityContext mActivity;
     private Drawable mIcon;
-    private final boolean mCenterVertically;
+    private boolean mCenterVertically;
 
     private final int mDisplay;
 
     private final CheckLongPressHelper mLongPressHelper;
-    private final StylusEventHelper mStylusEventHelper;
-    private final float mSlop;
 
     private final boolean mLayoutHorizontal;
     private final int mIconSize;
@@ -135,9 +145,6 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private final boolean mIgnorePaddingTouch;
-
     private IconLoadRequest mIconLoadRequest;
 
     public BubbleTextView(Context context) {
@@ -151,36 +158,29 @@
     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mActivity = ActivityContext.lookupContext(context);
-        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
 
         TypedArray a = context.obtainStyledAttributes(attrs,
                 R.styleable.BubbleTextView, defStyle, 0);
         mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
+        DeviceProfile grid = mActivity.getDeviceProfile();
 
         mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
         final int defaultIconSize;
         if (mDisplay == DISPLAY_WORKSPACE) {
-            DeviceProfile grid = mActivity.getWallpaperDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
-            mIgnorePaddingTouch = true;
         } else if (mDisplay == DISPLAY_ALL_APPS) {
-            DeviceProfile grid = mActivity.getDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
             defaultIconSize = grid.allAppsIconSizePx;
-            mIgnorePaddingTouch = true;
         } else if (mDisplay == DISPLAY_FOLDER) {
-            DeviceProfile grid = mActivity.getDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
             setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
             defaultIconSize = grid.folderChildIconSizePx;
-            mIgnorePaddingTouch = true;
         } else {
             // widget_selection or shortcut_popup
-            defaultIconSize = mActivity.getDeviceProfile().iconSizePx;
-            mIgnorePaddingTouch = false;
+            defaultIconSize = grid.iconSizePx;
         }
 
         mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
@@ -190,7 +190,6 @@
         a.recycle();
 
         mLongPressHelper = new CheckLongPressHelper(this);
-        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
 
         mDotParams = new DotRenderer.DrawParams();
 
@@ -215,6 +214,7 @@
         cancelDotScaleAnim();
         mDotParams.scale = 0f;
         mForceHideDot = false;
+        setBackground(null);
     }
 
     private void cancelDotScaleAnim() {
@@ -287,9 +287,8 @@
     }
 
     private void applyIconAndLabel(ItemInfoWithIcon info) {
-        FastBitmapDrawable iconDrawable = DrawableFactory.INSTANCE.get(getContext())
-                .newIcon(getContext(), info);
-        mDotParams.color = IconPalette.getMutedColor(info.iconColor, 0.54f);
+        FastBitmapDrawable iconDrawable = newIcon(getContext(), info);
+        mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
 
         setIcon(iconDrawable);
         setText(info.title);
@@ -331,42 +330,21 @@
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         // ignore events if they happen in padding area
-        if (event.getAction() == MotionEvent.ACTION_DOWN && mIgnorePaddingTouch
+        if (event.getAction() == MotionEvent.ACTION_DOWN
                 && (event.getY() < getPaddingTop()
                 || event.getX() < getPaddingLeft()
                 || event.getY() > getHeight() - getPaddingBottom()
                 || event.getX() > getWidth() - getPaddingRight())) {
             return false;
         }
-
-        // Call the superclass onTouchEvent first, because sometimes it changes the state to
-        // isPressed() on an ACTION_UP
-        boolean result = super.onTouchEvent(event);
-
-        // Check for a stylus button press, if it occurs cancel any long press checks.
-        if (mStylusEventHelper.onMotionEvent(event)) {
-            mLongPressHelper.cancelLongPress();
-            result = true;
+        if (isLongClickable()) {
+            super.onTouchEvent(event);
+            mLongPressHelper.onTouchEvent(event);
+            // Keep receiving the rest of the events
+            return true;
+        } else {
+            return super.onTouchEvent(event);
         }
-
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                // If we're in a stylus button press, don't check for long press.
-                if (!mStylusEventHelper.inStylusButtonPressed()) {
-                    mLongPressHelper.postCheckForLongPress();
-                }
-                break;
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                mLongPressHelper.cancelLongPress();
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
-                    mLongPressHelper.cancelLongPress();
-                }
-                break;
-        }
-        return result;
     }
 
     void setStayPressed(boolean stayPressed) {
@@ -496,7 +474,8 @@
         // Text should be visible everywhere but the hotseat.
         Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
         ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
-        return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+        return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
     }
 
     public void setTextVisibility(boolean visible) {
@@ -528,7 +507,6 @@
     @Override
     public void cancelLongPress() {
         super.cancelLongPress();
-
         mLongPressHelper.cancelLongPress();
     }
 
@@ -567,8 +545,7 @@
                     preloadDrawable = (PreloadIconDrawable) mIcon;
                     preloadDrawable.setLevel(progressLevel);
                 } else {
-                    preloadDrawable = DrawableFactory.INSTANCE.get(getContext())
-                            .newPendingIcon(getContext(), info);
+                    preloadDrawable = newPendingIcon(getContext(), info);
                     preloadDrawable.setLevel(progressLevel);
                     setIcon(preloadDrawable);
                 }
@@ -665,7 +642,7 @@
             mDisableRelayout = true;
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
-            info.iconBitmap.prepareToDraw();
+            info.bitmap.icon.prepareToDraw();
 
             if (info instanceof AppInfo) {
                 applyFromApplicationInfo((AppInfo) info);
@@ -700,4 +677,80 @@
     public int getIconSize() {
         return mIconSize;
     }
+
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
+    }
+
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
+    }
+
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
+        super.setScaleX(scale);
+        super.setScaleY(scale);
+    }
+
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getWorkspaceVisualDragBounds(Rect bounds) {
+        DeviceProfile grid = mActivity.getDeviceProfile();
+        BubbleTextView.getIconBounds(this, bounds, grid.iconSizePx);
+    }
+
+    private int getIconSizeForDisplay(int display) {
+        DeviceProfile grid = mActivity.getDeviceProfile();
+        switch (display) {
+            case DISPLAY_ALL_APPS:
+                return grid.allAppsIconSizePx;
+            case DISPLAY_WORKSPACE:
+            case DISPLAY_FOLDER:
+            default:
+                return grid.iconSizePx;
+        }
+    }
+
+    public void getSourceVisualDragBounds(Rect bounds) {
+        BubbleTextView.getIconBounds(this, bounds, getIconSizeForDisplay(mDisplay));
+    }
+
+    @Override
+    public SafeCloseable prepareDrawDragView() {
+        if (getIcon() instanceof FastBitmapDrawable) {
+            FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
+            icon.setScale(1f);
+        }
+        setForceHideDot(true);
+        return () -> { };
+    }
 }
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 2b0da43..09827d6 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
@@ -155,7 +156,7 @@
 
     @Override
     public final void onDragEnter(DragObject d) {
-        if (!d.accessibleDrag && !mTextVisible) {
+        if (!mAccessibleDrag && !mTextVisible) {
             // Show tooltip
             hideTooltip();
 
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 976ccd5..89d768c 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -33,6 +33,7 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -53,22 +54,18 @@
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
-import com.android.launcher3.accessibility.FolderAccessibilityHelper;
-import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.DragPreviewProvider;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.CellAndSpan;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.ParcelableSparseArray;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.Transposable;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -78,10 +75,7 @@
 import java.util.Comparator;
 import java.util.Stack;
 
-public class CellLayout extends ViewGroup implements Transposable {
-    public static final int WORKSPACE_ACCESSIBILITY_DRAG = 2;
-    public static final int FOLDER_ACCESSIBILITY_DRAG = 1;
-
+public class CellLayout extends ViewGroup {
     private static final String TAG = "CellLayout";
     private static final boolean LOGD = false;
 
@@ -104,13 +98,19 @@
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     @Thunk final int[] mTmpPoint = new int[2];
     @Thunk final int[] mTempLocation = new int[2];
+    final PointF mTmpPointF = new PointF();
+
+    // Used to visualize / debug the Grid of the CellLayout
+    private static final boolean VISUALIZE_GRID = false;
+    private Rect mVisualizeGridRect = new Rect();
+    private Paint mVisualizeGridPaint = new Paint();
 
     private GridOccupancy mOccupied;
     private GridOccupancy mTmpOccupied;
 
     private OnTouchListener mInterceptTouchListener;
 
-    private final ArrayList<PreviewBackground> mFolderBackgrounds = new ArrayList<>();
+    private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
     final PreviewBackground mFolderLeaveBehind = new PreviewBackground();
 
     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
@@ -136,7 +136,7 @@
     private final Paint mDragOutlinePaint = new Paint();
 
     @Thunk final ArrayMap<LayoutParams, Animator> mReorderAnimators = new ArrayMap<>();
-    @Thunk final ArrayMap<View, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
+    @Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
 
     private boolean mItemPlacementDirty = false;
 
@@ -179,12 +179,10 @@
 
     private final Rect mTempRect = new Rect();
 
-    private final static Paint sPaint = new Paint();
+    private static final Paint sPaint = new Paint();
 
     // Related to accessible drag and drop
-    private DragAndDropAccessibilityDelegate mTouchHelper;
-    private boolean mUseTouchHelper = false;
-    private RotationMode mRotationMode = RotationMode.NORMAL;
+    DragAndDropAccessibilityDelegate mTouchHelper;
 
     public CellLayout(Context context) {
         this(context, null);
@@ -206,7 +204,7 @@
         setClipToPadding(false);
         mActivity = ActivityContext.lookupContext(context);
 
-        DeviceProfile grid = mActivity.getWallpaperDeviceProfile();
+        DeviceProfile grid = mActivity.getDeviceProfile();
 
         mCellWidth = mCellHeight = -1;
         mFixedCellWidth = mFixedCellHeight = -1;
@@ -219,8 +217,8 @@
         mPreviousReorderDirection[0] = INVALID_DIRECTION;
         mPreviousReorderDirection[1] = INVALID_DIRECTION;
 
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
 
         setAlwaysDrawnWithCacheEnabled(false);
         final Resources res = getResources();
@@ -292,26 +290,18 @@
         addView(mShortcutsAndWidgets);
     }
 
-    public void enableAccessibleDrag(boolean enable, int dragType) {
-        mUseTouchHelper = enable;
-        if (!enable) {
-            ViewCompat.setAccessibilityDelegate(this, null);
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-            setOnClickListener(null);
-        } else {
-            if (dragType == WORKSPACE_ACCESSIBILITY_DRAG &&
-                    !(mTouchHelper instanceof WorkspaceAccessibilityHelper)) {
-                mTouchHelper = new WorkspaceAccessibilityHelper(this);
-            } else if (dragType == FOLDER_ACCESSIBILITY_DRAG &&
-                    !(mTouchHelper instanceof FolderAccessibilityHelper)) {
-                mTouchHelper = new FolderAccessibilityHelper(this);
-            }
-            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            setOnClickListener(mTouchHelper);
-        }
+    /**
+     * Sets or clears a delegate used for accessible drag and drop
+     */
+    public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) {
+        setOnClickListener(delegate);
+        ViewCompat.setAccessibilityDelegate(this, delegate);
+
+        mTouchHelper = delegate;
+        int accessibilityFlag = mTouchHelper != null
+                ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO;
+        setImportantForAccessibility(accessibilityFlag);
+        getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
 
         // Invalidate the accessibility hierarchy
         if (getParent() != null) {
@@ -320,28 +310,10 @@
         }
     }
 
-    public void setRotationMode(RotationMode mode) {
-        if (mRotationMode != mode) {
-            mRotationMode = mode;
-            requestLayout();
-        }
-    }
-
-    @Override
-    public RotationMode getRotationMode() {
-        return mRotationMode;
-    }
-
-    @Override
-    public void setPadding(int left, int top, int right, int bottom) {
-        mRotationMode.mapRect(left, top, right, bottom, mTempRect);
-        super.setPadding(mTempRect.left, mTempRect.top, mTempRect.right, mTempRect.bottom);
-    }
-
     @Override
     public boolean dispatchHoverEvent(MotionEvent event) {
         // Always attempt to dispatch hover events to accessibility first.
-        if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) {
+        if (mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)) {
             return true;
         }
         return super.dispatchHoverEvent(event);
@@ -349,8 +321,8 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (mUseTouchHelper ||
-                (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
+        if (mTouchHelper != null
+                || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
             return true;
         }
         return false;
@@ -466,49 +438,82 @@
             }
         }
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
+            cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
-            bg.drawBackground(canvas);
-            if (!bg.isClipping) {
-                bg.drawBackgroundStroke(canvas);
-            }
+            cellDrawing.drawUnderItem(canvas);
             canvas.restore();
         }
 
-        if (mFolderLeaveBehind.delegateCellX >= 0 && mFolderLeaveBehind.delegateCellY >= 0) {
-            cellToPoint(mFolderLeaveBehind.delegateCellX,
-                    mFolderLeaveBehind.delegateCellY, mTempLocation);
+        if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
+            cellToPoint(mFolderLeaveBehind.mDelegateCellX,
+                    mFolderLeaveBehind.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
             mFolderLeaveBehind.drawLeaveBehind(canvas);
             canvas.restore();
         }
+
+        if (VISUALIZE_GRID) {
+            visualizeGrid(canvas);
+        }
+    }
+
+    protected void visualizeGrid(Canvas canvas) {
+        mVisualizeGridRect.set(0, 0, mCellWidth, mCellHeight);
+        mVisualizeGridPaint.setStrokeWidth(4);
+
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                canvas.save();
+
+                int transX = i * mCellWidth;
+                int transY = j * mCellHeight;
+
+                canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
+
+                mVisualizeGridPaint.setStyle(Paint.Style.FILL);
+                mVisualizeGridPaint.setColor(Color.argb(80, 255, 100, 100));
+
+                canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
+
+                mVisualizeGridPaint.setStyle(Paint.Style.STROKE);
+                mVisualizeGridPaint.setColor(Color.argb(255, 255, 100, 100));
+
+                canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
+                canvas.restore();
+            }
+        }
     }
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            if (bg.isClipping) {
-                cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
-                canvas.save();
-                canvas.translate(mTempLocation[0], mTempLocation[1]);
-                bg.drawBackgroundStroke(canvas);
-                canvas.restore();
-            }
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
+            cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
+            canvas.save();
+            canvas.translate(mTempLocation[0], mTempLocation[1]);
+            bg.drawOverItem(canvas);
+            canvas.restore();
         }
     }
 
-    public void addFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.add(bg);
+    /**
+     * Add Delegated cell drawing
+     */
+    public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.add(bg);
     }
-    public void removeFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.remove(bg);
+
+    /**
+     * Remove item from DelegatedCellDrawings
+     */
+    public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.remove(bg);
     }
 
     public void setFolderLeaveBehindCell(int x, int y) {
@@ -516,14 +521,14 @@
         mFolderLeaveBehind.setup(getContext(), mActivity, null,
                 child.getMeasuredWidth(), child.getPaddingTop());
 
-        mFolderLeaveBehind.delegateCellX = x;
-        mFolderLeaveBehind.delegateCellY = y;
+        mFolderLeaveBehind.mDelegateCellX = x;
+        mFolderLeaveBehind.mDelegateCellY = y;
         invalidate();
     }
 
     public void clearFolderLeaveBehind() {
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
         invalidate();
     }
 
@@ -536,7 +541,7 @@
         try {
             dispatchRestoreInstanceState(states);
         } catch (IllegalArgumentException ex) {
-            if (FeatureFlags.IS_DOGFOOD_BUILD) {
+            if (FeatureFlags.IS_STUDIO_BUILD) {
                 throw ex;
             }
             // Mismatched viewId / viewType preventing restore. Skip restore on production builds.
@@ -771,13 +776,6 @@
         int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight());
         int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
 
-        mShortcutsAndWidgets.setRotation(mRotationMode.surfaceRotation);
-        if (mRotationMode.isTransposed) {
-            int tmp = childWidthSize;
-            childWidthSize = childHeightSize;
-            childHeightSize = tmp;
-        }
-
         if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
             int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
             int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY);
@@ -828,15 +826,7 @@
                 right + mTempRect.right + getPaddingRight(),
                 bottom + mTempRect.bottom + getPaddingBottom());
 
-        if (mRotationMode.isTransposed) {
-            int halfW = mShortcutsAndWidgets.getMeasuredWidth() / 2;
-            int halfH = mShortcutsAndWidgets.getMeasuredHeight() / 2;
-            int cX = (left + right) / 2;
-            int cY = (top + bottom) / 2;
-            mShortcutsAndWidgets.layout(cX - halfW, cY - halfH, cX + halfW, cY + halfH);
-        } else {
-            mShortcutsAndWidgets.layout(left, top, right, bottom);
-        }
+        mShortcutsAndWidgets.layout(left, top, right, bottom);
     }
 
     /**
@@ -845,8 +835,7 @@
      * width in {@link DeviceProfile#calculateCellWidth(int, int)}.
      */
     public int getUnusedHorizontalSpace() {
-        return (mRotationMode.isTransposed ? getMeasuredHeight() : getMeasuredWidth())
-                - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
+        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
     }
 
     public Drawable getScrimBackground() {
@@ -870,9 +859,10 @@
             int delay, boolean permanent, boolean adjustOccupied) {
         ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
 
-        if (clc.indexOfChild(child) != -1) {
+        if (clc.indexOfChild(child) != -1 && (child instanceof Reorderable)) {
             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
             final ItemInfo info = (ItemInfo) child.getTag();
+            final Reorderable item = (Reorderable) child;
 
             // We cancel any existing animations
             if (mReorderAnimators.containsKey(lp)) {
@@ -880,13 +870,18 @@
                 mReorderAnimators.remove(lp);
             }
 
-            final int oldX = lp.x;
-            final int oldY = lp.y;
+
             if (adjustOccupied) {
                 GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied;
                 occupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false);
                 occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true);
             }
+
+            // Compute the new x and y position based on the new cellX and cellY
+            // We leverage the actual layout logic in the layout params and hence need to modify
+            // state and revert that state.
+            final int oldX = lp.x;
+            final int oldY = lp.y;
             lp.isLockedToGrid = true;
             if (permanent) {
                 lp.cellX = info.cellX = cellX;
@@ -896,15 +891,23 @@
                 lp.tmpCellY = cellY;
             }
             clc.setupLp(child);
-            lp.isLockedToGrid = false;
             final int newX = lp.x;
             final int newY = lp.y;
-
             lp.x = oldX;
             lp.y = oldY;
+            lp.isLockedToGrid = false;
+            // End compute new x and y
+
+            item.getReorderPreviewOffset(mTmpPointF);
+            final float initPreviewOffsetX = mTmpPointF.x;
+            final float initPreviewOffsetY = mTmpPointF.y;
+            final float finalPreviewOffsetX = newX - oldX;
+            final float finalPreviewOffsetY = newY - oldY;
+
 
             // Exit early if we're not actually moving the view
-            if (oldX == newX && oldY == newY) {
+            if (finalPreviewOffsetX == 0 && finalPreviewOffsetY == 0
+                    && initPreviewOffsetX == 0 && initPreviewOffsetY == 0) {
                 lp.isLockedToGrid = true;
                 return true;
             }
@@ -917,9 +920,9 @@
                 @Override
                 public void onAnimationUpdate(ValueAnimator animation) {
                     float r = (Float) animation.getAnimatedValue();
-                    lp.x = (int) ((1 - r) * oldX + r * newX);
-                    lp.y = (int) ((1 - r) * oldY + r * newY);
-                    child.requestLayout();
+                    float x = (1 - r) * initPreviewOffsetX + r * finalPreviewOffsetX;
+                    float y = (1 - r) * initPreviewOffsetY + r * finalPreviewOffsetY;
+                    item.setReorderPreviewOffset(x, y);
                 }
             });
             va.addListener(new AnimatorListenerAdapter() {
@@ -930,6 +933,7 @@
                     // place just yet.
                     if (!cancelled) {
                         lp.isLockedToGrid = true;
+                        item.setReorderPreviewOffset(0, 0);
                         child.requestLayout();
                     }
                     if (mReorderAnimators.containsKey(lp)) {
@@ -947,8 +951,8 @@
         return false;
     }
 
-    void visualizeDropLocation(View v, DragPreviewProvider outlineProvider, int cellX, int cellY,
-            int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
+    void visualizeDropLocation(DraggableView v, DragPreviewProvider outlineProvider, int cellX, int
+            cellY, int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
         final int oldDragCellX = mDragCell[0];
         final int oldDragCellY = mDragCell[1];
 
@@ -958,9 +962,6 @@
 
         Bitmap dragOutline = outlineProvider.generatedDragOutline;
         if (cellX != oldDragCellX || cellY != oldDragCellY) {
-            Point dragOffset = dragObject.dragView.getDragVisualizeOffset();
-            Rect dragRegion = dragObject.dragView.getDragRegion();
-
             mDragCell[0] = cellX;
             mDragCell[1] = cellY;
 
@@ -969,50 +970,32 @@
             mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
             Rect r = mDragOutlines[mDragOutlineCurrent];
 
+            cellToRect(cellX, cellY, spanX, spanY, r);
+            int left = r.left;
+            int top = r.top;
+
+            int width = dragOutline.getWidth();
+            int height = dragOutline.getHeight();
+
             if (resize) {
-                cellToRect(cellX, cellY, spanX, spanY, r);
-                if (v instanceof LauncherAppWidgetHostView) {
-                    DeviceProfile profile = mActivity.getWallpaperDeviceProfile();
-                    Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
-                }
-            } else {
-                // Find the top left corner of the rect the object will occupy
-                final int[] topLeft = mTmpPoint;
-                cellToPoint(cellX, cellY, topLeft);
-
-                int left = topLeft[0];
-                int top = topLeft[1];
-
-                if (v != null && dragOffset == null) {
-                    // When drawing the drag outline, it did not account for margin offsets
-                    // added by the view's parent.
-                    MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
-                    left += lp.leftMargin;
-                    top += lp.topMargin;
-
-                    // Offsets due to the size difference between the View and the dragOutline.
-                    // There is a size difference to account for the outer blur, which may lie
-                    // outside the bounds of the view.
-                    top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
-                    // We center about the x axis
-                    left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
-                } else {
-                    if (dragOffset != null && dragRegion != null) {
-                        // Center the drag region *horizontally* in the cell and apply a drag
-                        // outline offset
-                        left += dragOffset.x + ((mCellWidth * spanX) - dragRegion.width()) / 2;
-                        int cHeight = getShortcutsAndWidgets().getCellContentHeight();
-                        int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
-                        top += dragOffset.y + cellPaddingY;
-                    } else {
-                        // Center the drag outline in the cell
-                        left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
-                        top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
-                    }
-                }
-                r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight());
+                width = r.width();
+                height = r.height();
             }
 
+            // Center horizontaly
+            left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+
+            if (v != null && v.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
+                // Center vertically
+                top += ((mCellHeight * spanY) - 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));
+                top += cellPaddingY;
+            }
+
+            r.set(left, top, left + width, top + height);
+
             Utilities.scaleRectAboutCenter(r, mChildScale);
             mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
             mDragOutlineAnims[mDragOutlineCurrent].animateIn();
@@ -1898,7 +1881,7 @@
 
     // This method starts or changes the reorder preview animations
     private void beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution,
-            View dragView, int delay, int mode) {
+            View dragView, int mode) {
         int childCount = mShortcutsAndWidgets.getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = mShortcutsAndWidgets.getChildAt(i);
@@ -1907,10 +1890,11 @@
             boolean skip = mode == ReorderPreviewAnimation.MODE_HINT && solution.intersectingViews
                     != null && !solution.intersectingViews.contains(child);
 
+
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
-            if (c != null && !skip) {
-                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, lp.cellX,
-                        lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
+            if (c != null && !skip && (child instanceof Reorderable)) {
+                ReorderPreviewAnimation rha = new ReorderPreviewAnimation((Reorderable) child,
+                        mode, lp.cellX, lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
                 rha.animate();
             }
         }
@@ -1932,7 +1916,7 @@
     // Class which represents the reorder preview animations. These animations show that an item is
     // in a temporary state, and hint at where the item will return to.
     class ReorderPreviewAnimation {
-        final View child;
+        final Reorderable child;
         float finalDeltaX;
         float finalDeltaY;
         float initDeltaX;
@@ -1952,8 +1936,8 @@
         float animationProgress = 0;
         ValueAnimator a;
 
-        public ReorderPreviewAnimation(View child, int mode, int cellX0, int cellY0, int cellX1,
-                int cellY1, int spanX, int spanY) {
+        public ReorderPreviewAnimation(Reorderable child, int mode, int cellX0, int cellY0,
+                int cellX1, int cellY1, int spanX, int spanY) {
             regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
             final int x0 = mTmpPoint[0];
             final int y0 = mTmpPoint[1];
@@ -1965,61 +1949,60 @@
 
             this.child = child;
             this.mode = mode;
-            setInitialAnimationValues(false);
-            finalScale = (mChildScale - (CHILD_DIVIDEND / child.getWidth())) * initScale;
-            finalDeltaX = initDeltaX;
-            finalDeltaY = initDeltaY;
+            finalDeltaX = 0;
+            finalDeltaY = 0;
+
+            child.getReorderBounceOffset(mTmpPointF);
+            initDeltaX = mTmpPointF.x;
+            initDeltaY = mTmpPointF.y;
+            initScale = child.getReorderBounceScale();
+            finalScale = mChildScale - (CHILD_DIVIDEND / child.getView().getWidth()) * initScale;
+
             int dir = mode == MODE_HINT ? -1 : 1;
             if (dX == dY && dX == 0) {
             } else {
                 if (dY == 0) {
-                    finalDeltaX += - dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
+                    finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
                 } else if (dX == 0) {
-                    finalDeltaY += - dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
+                    finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
                 } else {
                     double angle = Math.atan( (float) (dY) / dX);
-                    finalDeltaX += (int) (- dir * Math.signum(dX) *
-                            Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
-                    finalDeltaY += (int) (- dir * Math.signum(dY) *
-                            Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
+                    finalDeltaX = (int) (-dir * Math.signum(dX)
+                            * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
+                    finalDeltaY = (int) (-dir * Math.signum(dY)
+                            * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
                 }
             }
         }
 
-        void setInitialAnimationValues(boolean restoreOriginalValues) {
-            if (restoreOriginalValues) {
-                if (child instanceof LauncherAppWidgetHostView) {
-                    LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
-                    initScale = lahv.getScaleToFit();
-                    initDeltaX = lahv.getTranslationForCentering().x;
-                    initDeltaY = lahv.getTranslationForCentering().y;
-                } else {
-                    initScale = mChildScale;
-                    initDeltaX = 0;
-                    initDeltaY = 0;
-                }
-            } else {
-                initScale = child.getScaleX();
-                initDeltaX = child.getTranslationX();
-                initDeltaY = child.getTranslationY();
-            }
+        void setInitialAnimationValuesToBaseline() {
+            initScale = mChildScale;
+            initDeltaX = 0;
+            initDeltaY = 0;
         }
 
         void animate() {
-            boolean noMovement = (finalDeltaX == initDeltaX) && (finalDeltaY == initDeltaY);
+            boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);
 
             if (mShakeAnimators.containsKey(child)) {
                 ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
-                oldAnimation.cancel();
                 mShakeAnimators.remove(child);
+
                 if (noMovement) {
-                    completeAnimationImmediately();
+                    // A previous animation for this item exists, and no new animation will exist.
+                    // Finish the old animation smoothly.
+                    oldAnimation.finishAnimation();
                     return;
+                } else {
+                    // A previous animation for this item exists, and a new one will exist. Stop
+                    // the old animation in its tracks, and proceed with the new one.
+                    oldAnimation.cancel();
                 }
             }
             if (noMovement) {
                 return;
             }
+
             ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, 0, 1);
             a = va;
 
@@ -2036,7 +2019,7 @@
             va.addListener(new AnimatorListenerAdapter() {
                 public void onAnimationRepeat(Animator animation) {
                     // We make sure to end only after a full period
-                    setInitialAnimationValues(true);
+                    setInitialAnimationValuesToBaseline();
                     repeating = true;
                 }
             });
@@ -2049,11 +2032,9 @@
             float r1 = (mode == MODE_HINT && repeating) ? 1.0f : animationProgress;
             float x = r1 * finalDeltaX + (1 - r1) * initDeltaX;
             float y = r1 * finalDeltaY + (1 - r1) * initDeltaY;
-            child.setTranslationX(x);
-            child.setTranslationY(y);
+            child.setReorderBounceOffset(x, y);
             float s = animationProgress * finalScale + (1 - animationProgress) * initScale;
-            child.setScaleX(s);
-            child.setScaleY(s);
+            child.setReorderBounceScale(s);
         }
 
         private void cancel() {
@@ -2062,27 +2043,27 @@
             }
         }
 
-        @Thunk void completeAnimationImmediately() {
+        /**
+         * Smoothly returns the item to its baseline position / scale
+         */
+        @Thunk void finishAnimation() {
             if (a != null) {
                 a.cancel();
             }
 
-            setInitialAnimationValues(true);
-            a = new PropertyListBuilder()
-                    .scale(initScale)
-                    .translationX(initDeltaX)
-                    .translationY(initDeltaY)
-                    .build(child)
-                    .setDuration(REORDER_ANIMATION_DURATION);
-            Launcher.cast(mActivity).getDragController().addFirstFrameAnimationHelper(a);
+            setInitialAnimationValuesToBaseline();
+            ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS,
+                    animationProgress, 0);
+            a = va;
             a.setInterpolator(DEACCEL_1_5);
+            a.setDuration(REORDER_ANIMATION_DURATION);
             a.start();
         }
     }
 
     private void completeAndClearReorderPreviewAnimations() {
         for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
-            a.completeAnimationImmediately();
+            a.finishAnimation();
         }
         mShakeAnimators.clear();
     }
@@ -2160,6 +2141,8 @@
     */
     private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
             int spanY, View dragView, int[] resultDirection) {
+
+        //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
         int[] targetDestination = new int[2];
 
         findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
@@ -2270,7 +2253,7 @@
                 setItemPlacementDirty(false);
             } else {
                 beginOrAdjustReorderPreviewAnimations(swapSolution, dragView,
-                        REORDER_ANIMATION_DURATION, ReorderPreviewAnimation.MODE_PREVIEW);
+                        ReorderPreviewAnimation.MODE_PREVIEW);
             }
             mShortcutsAndWidgets.requestLayout();
         }
@@ -2324,7 +2307,7 @@
 
         if (mode == MODE_SHOW_REORDER_HINT) {
             if (finalSolution != null) {
-                beginOrAdjustReorderPreviewAnimations(finalSolution, dragView, 0,
+                beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
                         ReorderPreviewAnimation.MODE_HINT);
                 result[0] = finalSolution.cellX;
                 result[1] = finalSolution.cellY;
@@ -2364,7 +2347,7 @@
                     setItemPlacementDirty(false);
                 } else {
                     beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
-                            REORDER_ANIMATION_DURATION,  ReorderPreviewAnimation.MODE_PREVIEW);
+                            ReorderPreviewAnimation.MODE_PREVIEW);
                 }
             }
         } else {
@@ -2744,6 +2727,24 @@
     }
 
     /**
+     * A Delegated cell Drawing for drawing on CellLayout
+     */
+    public abstract static class DelegatedCellDrawing {
+        public int mDelegateCellX;
+        public int mDelegateCellY;
+
+        /**
+         * Draw under CellLayout
+         */
+        public abstract void drawUnderItem(Canvas canvas);
+
+        /**
+         * Draw over CellLayout
+         */
+        public abstract void drawOverItem(Canvas canvas);
+    }
+
+    /**
      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
      * if necessary).
      */
@@ -2763,6 +2764,36 @@
         return false;
     }
 
+    /**
+     * Finds solution to accept hotseat migration to cell layout. commits solution if commitConfig
+     */
+    public boolean makeSpaceForHotseatMigration(boolean commitConfig) {
+        int[] cellPoint = new int[2];
+        int[] directionVector = new int[]{0, -1};
+        cellToPoint(0, mCountY, cellPoint);
+        ItemConfiguration configuration = new ItemConfiguration();
+        if (findReorderSolution(cellPoint[0], cellPoint[1], mCountX, 1, mCountX, 1,
+                directionVector, null, false, configuration).isSolution) {
+            if (commitConfig) {
+                copySolutionToTempState(configuration, null);
+                commitTempPlacement();
+                // undo marking cells occupied since there is actually nothing being placed yet.
+                mOccupied.markCells(0, mCountY - 1, mCountX, 1, false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * returns a copy of cell layout's grid occupancy
+     */
+    public GridOccupancy cloneGridOccupancy() {
+        GridOccupancy occupancy = new GridOccupancy(mCountX, mCountY);
+        mOccupied.copyTo(occupancy);
+        return occupancy;
+    }
+
     public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
         return mOccupied.isRegionVacant(x, y, spanX, spanY);
     }
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index 639c173..ff405ec 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -16,46 +16,68 @@
 
 package com.android.launcher3;
 
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 
-import com.android.launcher3.util.Thunk;
-
+/**
+ * Utility class to handle tripper long press on a view with custom timeout and stylus event
+ */
 public class CheckLongPressHelper {
 
     public static final float DEFAULT_LONG_PRESS_TIMEOUT_FACTOR = 0.75f;
 
-    @Thunk View mView;
-    @Thunk View.OnLongClickListener mListener;
-    @Thunk boolean mHasPerformedLongPress;
-    private float mLongPressTimeoutFactor = DEFAULT_LONG_PRESS_TIMEOUT_FACTOR;
-    private CheckForLongPress mPendingCheckForLongPress;
+    private final View mView;
+    private final View.OnLongClickListener mListener;
+    private final float mSlop;
 
-    class CheckForLongPress implements Runnable {
-        public void run() {
-            if ((mView.getParent() != null) && mView.hasWindowFocus()
-                    && !mHasPerformedLongPress) {
-                boolean handled;
-                if (mListener != null) {
-                    handled = mListener.onLongClick(mView);
-                } else {
-                    handled = mView.performLongClick();
-                }
-                if (handled) {
-                    mView.setPressed(false);
-                    mHasPerformedLongPress = true;
-                }
-            }
-        }
-    }
+    private float mLongPressTimeoutFactor = DEFAULT_LONG_PRESS_TIMEOUT_FACTOR;
+
+    private boolean mHasPerformedLongPress;
+
+    private Runnable mPendingCheckForLongPress;
 
     public CheckLongPressHelper(View v) {
-        mView = v;
+        this(v, null);
     }
 
     public CheckLongPressHelper(View v, View.OnLongClickListener listener) {
         mView = v;
         mListener = listener;
+        mSlop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
+    }
+
+    /**
+     * Handles the touch event on a view
+     *
+     * @see View#onTouchEvent(MotionEvent)
+     */
+    public void onTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                // Just in case the previous long press hasn't been cleared, we make sure to
+                // start fresh on touch down.
+                cancelLongPress();
+
+                postCheckForLongPress();
+                if (isStylusButtonPressed(ev)) {
+                    triggerLongPress();
+                }
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                cancelLongPress();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!Utilities.pointInView(mView, ev.getX(), ev.getY(), mSlop)) {
+                    cancelLongPress();
+                } else if (mPendingCheckForLongPress != null && isStylusButtonPressed(ev)) {
+                    // Only trigger long press if it has not been cancelled before
+                    triggerLongPress();
+                }
+                break;
+        }
     }
 
     /**
@@ -65,25 +87,67 @@
         mLongPressTimeoutFactor = longPressTimeoutFactor;
     }
 
-    public void postCheckForLongPress() {
+    private void postCheckForLongPress() {
         mHasPerformedLongPress = false;
 
         if (mPendingCheckForLongPress == null) {
-            mPendingCheckForLongPress = new CheckForLongPress();
+            mPendingCheckForLongPress = this::triggerLongPress;
         }
         mView.postDelayed(mPendingCheckForLongPress,
                 (long) (ViewConfiguration.getLongPressTimeout() * mLongPressTimeoutFactor));
     }
 
+    /**
+     * Cancels any pending long press
+     */
     public void cancelLongPress() {
         mHasPerformedLongPress = false;
+        clearCallbacks();
+    }
+
+    /**
+     * Returns true if long press has been performed in the current touch gesture
+     */
+    public boolean hasPerformedLongPress() {
+        return mHasPerformedLongPress;
+    }
+
+    private void triggerLongPress() {
+        if ((mView.getParent() != null)
+                && mView.hasWindowFocus()
+                && (!mView.isPressed() || mListener == null)
+                && !mHasPerformedLongPress) {
+            boolean handled;
+            if (mListener != null) {
+                handled = mListener.onLongClick(mView);
+            } else {
+                handled = mView.performLongClick();
+            }
+            if (handled) {
+                mView.setPressed(false);
+                mHasPerformedLongPress = true;
+            }
+            clearCallbacks();
+        }
+    }
+
+    private void clearCallbacks() {
         if (mPendingCheckForLongPress != null) {
             mView.removeCallbacks(mPendingCheckForLongPress);
             mPendingCheckForLongPress = null;
         }
     }
 
-    public boolean hasPerformedLongPress() {
-        return mHasPerformedLongPress;
+
+    /**
+     * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button
+     * pressed.
+     *
+     * @param event The event to check.
+     * @return Whether a stylus button press occurred.
+     */
+    private static boolean isStylusButtonPressed(MotionEvent event) {
+        return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
+                && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY);
     }
 }
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 3347b2a..2857497 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_CANCEL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.UNDO;
 
@@ -27,13 +29,20 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.ModelWriter;
+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.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.views.Snackbar;
 
 public class DeleteDropTarget extends ButtonDropTarget {
 
+    private final StatsLogManager mStatsLogManager;
+
     private int mControlType = ControlType.DEFAULT_CONTROLTYPE;
 
     public DeleteDropTarget(Context context, AttributeSet attrs) {
@@ -42,6 +51,7 @@
 
     public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        this.mStatsLogManager = StatsLogManager.newInstance(context);
     }
 
     @Override
@@ -67,7 +77,7 @@
     public boolean supportsAccessibilityDrop(ItemInfo info, View view) {
         if (info instanceof WorkspaceItemInfo) {
             // Support the action unless the item is in a context menu.
-            return info.screenId >= 0;
+            return canRemove(info);
         }
 
         return (info instanceof LauncherAppWidgetInfo)
@@ -113,8 +123,12 @@
     public void onDrop(DragObject d, DragOptions options) {
         if (canRemove(d.dragInfo)) {
             mLauncher.getModelWriter().prepareToUndoDelete();
+            d.dragInfo.container = NO_ID;
         }
         super.onDrop(d, options);
+        mStatsLogManager.logger().withInstanceId(d.logInstanceId)
+                .log(mControlType == ControlType.REMOVE_TARGET ? LAUNCHER_ITEM_DROPPED_ON_REMOVE
+                        : LAUNCHER_ITEM_DROPPED_ON_CANCEL);
     }
 
     @Override
@@ -125,7 +139,8 @@
             onAccessibilityDrop(null, item);
             ModelWriter modelWriter = mLauncher.getModelWriter();
             Runnable onUndoClicked = () -> {
-                modelWriter.abortDelete(itemPage);
+                mLauncher.setPageToBindSynchronously(itemPage);
+                modelWriter.abortDelete();
                 mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
             };
             Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index bc6fa6e..49caa93 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -22,22 +22,23 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.util.DisplayMetrics;
 import android.view.Surface;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.WindowBounds;
 
 public class DeviceProfile {
 
+    private static final float TABLET_MIN_DPS = 600;
+    private static final float LARGE_TABLET_MIN_DPS = 720;
+
+
     public final InvariantDeviceProfile inv;
-    // IDP with no grid override values.
-    @Nullable private final InvariantDeviceProfile originalIdp;
+    private final DefaultDisplay.Info mInfo;
 
     // Device properties
     public final boolean isTablet;
@@ -49,6 +50,8 @@
     public final boolean isLandscape;
     public final boolean isMultiWindowMode;
 
+    public final int windowX;
+    public final int windowY;
     public final int widthPx;
     public final int heightPx;
     public final int availableWidthPx;
@@ -76,9 +79,9 @@
     public float workspaceSpringLoadShrinkFactor;
     public final int workspaceSpringLoadedBottomSpace;
 
-    // Drag handle
-    public final int verticalDragHandleSizePx;
-    private final int verticalDragHandleOverlapWorkspace;
+    // Workspace page indicator
+    public final int workspacePageIndicatorHeight;
+    private final int mWorkspacePageIndicatorOverlapWorkspace;
 
     // Workspace icons
     public int iconSizePx;
@@ -137,14 +140,16 @@
     public DotRenderer mDotRendererWorkSpace;
     public DotRenderer mDotRendererAllApps;
 
-    public DeviceProfile(Context context, InvariantDeviceProfile inv,
-            InvariantDeviceProfile originalIDP, Point minSize, Point maxSize,
-            int width, int height, boolean isLandscape, boolean isMultiWindowMode) {
+    DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
+            Point minSize, Point maxSize, int width, int height, boolean isLandscape,
+            boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
+            Point windowPosition) {
 
         this.inv = inv;
-        this.originalIdp = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
+        windowX = windowPosition.x;
+        windowY = windowPosition.y;
 
         // Determine sizes.
         widthPx = width;
@@ -157,24 +162,24 @@
             availableHeightPx = maxSize.y;
         }
 
-        Resources res = context.getResources();
-        DisplayMetrics dm = res.getDisplayMetrics();
+        mInfo = info;
 
         // Constants from resources
-        isTablet = res.getBoolean(R.bool.is_tablet);
-        isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
+        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;
         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
-        transposeLayoutWithOrientation =
-                res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
+        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
 
-        context = getContext(context, isVerticalBarLayout()
+        context = getContext(context, info, isVerticalBarLayout()
                 ? Configuration.ORIENTATION_LANDSCAPE
                 : Configuration.ORIENTATION_PORTRAIT);
-        res = context.getResources();
+        final Resources res = context.getResources();
 
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
         desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
@@ -190,10 +195,10 @@
             cellLayoutBottomPaddingPx = 0;
         }
 
-        verticalDragHandleSizePx = res.getDimensionPixelSize(
-                R.dimen.vertical_drag_handle_size);
-        verticalDragHandleOverlapWorkspace =
-                res.getDimensionPixelSize(R.dimen.vertical_drag_handle_overlap_workspace);
+        workspacePageIndicatorHeight = res.getDimensionPixelSize(
+                R.dimen.workspace_page_indicator_height);
+        mWorkspacePageIndicatorOverlapWorkspace =
+                res.getDimensionPixelSize(R.dimen.workspace_page_indicator_overlap_workspace);
 
         iconDrawablePaddingOriginalPx =
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
@@ -211,14 +216,15 @@
         hotseatBarSidePaddingEndPx =
                 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() ? verticalDragHandleSizePx : 0;
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, dm) + (isVerticalBarLayout()
+        hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
+        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+                + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
                 : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
                         + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
 
         // Calculate all of the remaining variables.
-        updateAvailableDimensions(dm, res);
+        updateAvailableDimensions(res);
 
         // Now that we have all of the variables calculated, we can tune certain sizes.
         if (!isVerticalBarLayout() && isPhone && isTallDevice) {
@@ -227,25 +233,12 @@
             // 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
-                    - verticalDragHandleSizePx;
+                    - workspacePageIndicatorHeight;
             hotseatBarSizePx += extraSpace;
             hotseatBarBottomPaddingPx += extraSpace;
 
             // Recalculate the available dimensions using the new hotseat size.
-            updateAvailableDimensions(dm, res);
-        }
-
-        if (originalIDP != null) {
-            // Grid size change should not affect All Apps UI, so we use the original profile
-            // measurements here.
-            DeviceProfile originalProfile = isLandscape
-                    ? originalIDP.landscapeProfile
-                    : originalIDP.portraitProfile;
-            allAppsIconSizePx = originalProfile.iconSizePx;
-            allAppsIconTextSizePx = originalProfile.iconTextSizePx;
-            allAppsCellHeightPx = originalProfile.allAppsCellHeightPx;
-            allAppsIconDrawablePaddingPx = originalProfile.iconDrawablePaddingOriginalPx;
-            allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
+            updateAvailableDimensions(res);
         }
         updateWorkspacePadding();
 
@@ -257,22 +250,34 @@
                         IconShape.DEFAULT_PATH_SIZE);
     }
 
-    public DeviceProfile copy(Context context) {
+    public Builder toBuilder(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
-        return new DeviceProfile(context, inv, originalIdp, size, size, widthPx, heightPx,
-                isLandscape, isMultiWindowMode);
+        return new Builder(context, inv, mInfo)
+                .setSizeRange(size, size)
+                .setSize(widthPx, heightPx)
+                .setWindowPosition(windowX, windowY)
+                .setMultiWindowMode(isMultiWindowMode);
     }
 
-    public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
+    public DeviceProfile copy(Context context) {
+        return toBuilder(context).build();
+    }
+
+    /**
+     * TODO: Move this to the builder as part of setMultiWindowMode
+     */
+    public DeviceProfile getMultiWindowProfile(Context context, WindowBounds windowBounds) {
         // We take the minimum sizes of this profile and it's multi-window variant to ensure that
         // the system decor is always excluded.
-        mwSize.set(Math.min(availableWidthPx, mwSize.x), Math.min(availableHeightPx, mwSize.y));
+        Point mwSize = new Point(Math.min(availableWidthPx, windowBounds.availableSize.x),
+                Math.min(availableHeightPx, windowBounds.availableSize.y));
 
-        // In multi-window mode, we can have widthPx = availableWidthPx
-        // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
-        // widthPx and heightPx values where it's needed.
-        DeviceProfile profile = new DeviceProfile(context, inv, originalIdp, mwSize, mwSize,
-                mwSize.x, mwSize.y, isLandscape, true);
+        DeviceProfile profile = toBuilder(context)
+                .setSizeRange(mwSize, mwSize)
+                .setSize(windowBounds.bounds.width(), windowBounds.bounds.height())
+                .setWindowPosition(windowBounds.bounds.left, windowBounds.bounds.top)
+                .setMultiWindowMode(true)
+                .build();
 
         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@@ -292,7 +297,7 @@
     }
 
     /**
-     * Inverse of {@link #getMultiWindowProfile(Context, Point)}
+     * Inverse of {@link #getMultiWindowProfile(Context, WindowBounds)}
      * @return device profile corresponding to the current orientation in non multi-window mode.
      */
     public DeviceProfile getFullScreenProfile() {
@@ -307,27 +312,30 @@
         iconTextSizePx = 0;
         iconDrawablePaddingPx = 0;
         cellHeightPx = iconSizePx;
+        autoResizeAllAppsCells();
+    }
 
-        // In normal cases, All Apps cell height should equal the Workspace cell height.
-        // Since we are removing labels from the Workspace, we need to manually compute the
-        // All Apps cell height.
+    /**
+     * Re-computes the all-apps cell size to be independent of workspace
+     */
+    public void autoResizeAllAppsCells() {
         int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1);
         allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
                 + Utilities.calculateTextHeight(allAppsIconTextSizePx)
                 + topBottomPadding * 2;
     }
 
-    private void updateAvailableDimensions(DisplayMetrics dm, Resources res) {
-        updateIconSize(1f, res, dm);
+    private void 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, dm);
+            updateIconSize(scale, res);
         }
-        updateAvailableFolderCellDimensions(dm, res);
+        updateAvailableFolderCellDimensions(res);
     }
 
     /**
@@ -335,12 +343,13 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    private void updateIconSize(float scale, Resources res, DisplayMetrics dm) {
+    private 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, dm) * scale));
-        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);
+        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
+                * scale));
+        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
         cellHeightPx = iconSizePx + iconDrawablePaddingPx
@@ -356,10 +365,20 @@
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
-        allAppsIconSizePx = iconSizePx;
-        allAppsIconTextSizePx = iconTextSizePx;
-        allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
-        allAppsCellHeightPx = getCellSize().y;
+        // All apps
+        if (allAppsHasDifferentNumColumns()) {
+            allAppsIconSizePx = ResourceUtils.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.
+            allAppsCellHeightPx = Math.round(allAppsIconSizePx + allAppsIconTextSizePx
+                    + (4 * allAppsIconDrawablePaddingPx));
+        } else {
+            allAppsIconSizePx = iconSizePx;
+            allAppsIconTextSizePx = iconTextSizePx;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
+            allAppsCellHeightPx = getCellSize().y;
+        }
         allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
 
         if (isVerticalBarLayout()) {
@@ -376,7 +395,7 @@
 
         if (!isVerticalLayout) {
             int expectedWorkspaceHeight = availableHeightPx - hotseatBarSizePx
-                    - verticalDragHandleSizePx - edgeMarginPx;
+                    - workspacePageIndicatorHeight - edgeMarginPx;
             float minRequiredHeight = dropTargetBarSizePx + workspaceSpringLoadedBottomSpace;
             workspaceSpringLoadShrinkFactor = Math.min(
                     res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f,
@@ -391,12 +410,12 @@
         folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
     }
 
-    private void updateAvailableFolderCellDimensions(DisplayMetrics dm, Resources res) {
+    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, dm, res);
+        updateFolderCellSize(1f, res);
 
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
@@ -415,12 +434,12 @@
 
         float scale = Math.min(scaleX, scaleY);
         if (scale < 1f) {
-            updateFolderCellSize(scale, dm, res);
+            updateFolderCellSize(scale, res);
         }
     }
 
-    private void updateFolderCellSize(float scale, DisplayMetrics dm, Resources res) {
-        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, dm) * scale);
+    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);
 
@@ -480,14 +499,14 @@
             padding.bottom = edgeMarginPx;
             if (isSeascape()) {
                 padding.left = hotseatBarSizePx;
-                padding.right = verticalDragHandleSizePx;
+                padding.right = hotseatBarSidePaddingStartPx;
             } else {
-                padding.left = verticalDragHandleSizePx;
+                padding.left = hotseatBarSidePaddingStartPx;
                 padding.right = hotseatBarSizePx;
             }
         } else {
-            int paddingBottom = hotseatBarSizePx + verticalDragHandleSizePx
-                    - verticalDragHandleOverlapWorkspace;
+            int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
+                    - mWorkspacePageIndicatorOverlapWorkspace;
             if (isTablet) {
                 // Pad the left and right of the workspace to ensure consistent spacing
                 // between all icons
@@ -521,7 +540,6 @@
                         mInsets.right + hotseatBarSidePaddingStartPx, mInsets.bottom);
             }
         } else {
-
             // We want the edges of the hotseat to line up with the edges of the workspace, but the
             // icons in the hotseat are a different size, and so don't line up perfectly. To account
             // for this, we pad the left and right of the hotseat with half of the difference of a
@@ -530,9 +548,11 @@
             float hotseatCellWidth = (float) widthPx / inv.numHotseatIcons;
             int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
             mHotseatPadding.set(
-                    hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx,
+                    hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx
+                            + mInsets.left,
                     hotseatBarTopPaddingPx,
-                    hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx,
+                    hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx
+                            + mInsets.right,
                     hotseatBarBottomPaddingPx + mInsets.bottom + cellLayoutBottomPaddingPx);
         }
         return mHotseatPadding;
@@ -554,7 +574,7 @@
                     mInsets.top + dropTargetBarSizePx + edgeMarginPx,
                     mInsets.left + availableWidthPx - edgeMarginPx,
                     mInsets.top + availableHeightPx - hotseatBarSizePx
-                            - verticalDragHandleSizePx - edgeMarginPx);
+                            - workspacePageIndicatorHeight - edgeMarginPx);
         }
     }
 
@@ -618,10 +638,11 @@
         }
     }
 
-    private static Context getContext(Context c, int orientation) {
-        Configuration context = new Configuration(c.getResources().getConfiguration());
-        context.orientation = orientation;
-        return c.createConfigurationContext(context);
+    private static Context getContext(Context c, DefaultDisplay.Info info, int orientation) {
+        Configuration config = new Configuration(c.getResources().getConfiguration());
+        config.orientation = orientation;
+        config.densityDpi = info.metrics.densityDpi;
+        return c.createConfigurationContext(config);
     }
 
     /**
@@ -637,4 +658,64 @@
          */
         void onDeviceProfileChanged(DeviceProfile dp);
     }
+
+    public static class Builder {
+        private Context mContext;
+        private InvariantDeviceProfile mInv;
+        private DefaultDisplay.Info mInfo;
+
+        private final Point mWindowPosition = new Point();
+        private Point mMinSize, mMaxSize;
+        private int mWidth, mHeight;
+
+        private boolean mIsLandscape;
+        private boolean mIsMultiWindowMode = false;
+        private boolean mTransposeLayoutWithOrientation;
+
+        public Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info) {
+            mContext = context;
+            mInv = inv;
+            mInfo = info;
+            mTransposeLayoutWithOrientation = context.getResources()
+                    .getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
+        }
+
+        public Builder setSizeRange(Point minSize, Point maxSize) {
+            mMinSize = minSize;
+            mMaxSize = maxSize;
+            return this;
+        }
+
+        public Builder setSize(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+            mIsLandscape = mWidth > mHeight;
+            return this;
+        }
+
+        public Builder setMultiWindowMode(boolean isMultiWindowMode) {
+            mIsMultiWindowMode = isMultiWindowMode;
+            return this;
+        }
+
+        /**
+         * Sets the window position if not full-screen
+         */
+        public Builder setWindowPosition(int x, int y) {
+            mWindowPosition.set(x, y);
+            return this;
+        }
+
+        public Builder setTransposeLayoutWithOrientation(boolean transposeLayoutWithOrientation) {
+            mTransposeLayoutWithOrientation = transposeLayoutWithOrientation;
+            return this;
+        }
+
+        public DeviceProfile build() {
+            return new DeviceProfile(mContext, mInv, mInfo, mMinSize, mMaxSize,
+                    mWidth, mHeight, mIsLandscape, mIsMultiWindowMode,
+                    mTransposeLayoutWithOrientation, mWindowPosition);
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 763432d..b27abc4 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -16,11 +16,19 @@
 
 package com.android.launcher3;
 
+import android.content.Context;
 import android.graphics.Rect;
 
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.folder.FolderNameProvider;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.Executors;
 
 /**
  * Interface defining an object that can receive a drag.
@@ -56,9 +64,6 @@
         /** Where the drag originated */
         public DragSource dragSource = null;
 
-        /** The object is part of an accessible drag operation */
-        public boolean accessibleDrag;
-
         /** Indicates that the drag operation was cancelled */
         public boolean cancelled = false;
 
@@ -67,7 +72,23 @@
 
         public DragViewStateAnnouncer stateAnnouncer;
 
-        public DragObject() {
+        public FolderNameProvider folderNameProvider;
+
+        /** The source view (ie. icon, widget etc.) that is being dragged and which the
+         * DragView represents. May be an actual View class or a virtual stand-in */
+        public DraggableView originalView = null;
+
+        /** Used for matching DROP event with its corresponding DRAG event on the server side. */
+        public final InstanceId logInstanceId =
+                new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/)
+                    .newInstanceId();
+
+        public DragObject(Context context) {
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                Executors.MODEL_EXECUTOR.post(() -> {
+                    folderNameProvider = FolderNameProvider.newInstance(context);
+                });
+            }
         }
 
         /**
@@ -92,6 +113,18 @@
 
             return res;
         }
+
+
+        /**
+         * This is used to determine if an object is dropped at a different location than it was
+         * dragged from
+         */
+        public boolean isMoved() {
+            return dragInfo.cellX != originalDragInfo.cellX
+                    || dragInfo.cellY != originalDragInfo.cellY
+                    || dragInfo.screenId != originalDragInfo.screenId
+                    || dragInfo.container != originalDragInfo.container;
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 4e0f2e7..d64967b 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -29,6 +29,7 @@
 
 /**
  * The edit text that reports back when the back key has been pressed.
+ * Note: AppCompatEditText doesn't fully support #displayCompletions and #onCommitCompletion
  */
 public class ExtendedEditText extends EditText {
 
@@ -39,7 +40,7 @@
      * Implemented by listeners of the back key.
      */
     public interface OnBackKeyListener {
-        public boolean onBackKey();
+        boolean onBackKey();
     }
 
     private OnBackKeyListener mBackKeyListener;
@@ -85,12 +86,9 @@
         super.onLayout(changed, left, top, right, bottom);
         if (mShowImeAfterFirstLayout) {
             // soft input only shows one frame after the layout of the EditText happens,
-            post(new Runnable() {
-                @Override
-                public void run() {
-                    showSoftInput();
-                    mShowImeAfterFirstLayout = false;
-                }
+            post(() -> {
+                showSoftInput();
+                mShowImeAfterFirstLayout = false;
             });
         }
     }
@@ -105,7 +103,7 @@
 
     private boolean showSoftInput() {
         return requestFocus() &&
-                ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE))
+                getContext().getSystemService(InputMethodManager.class)
                     .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
     }
 
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index a90025e..d3b86de 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 
 import android.animation.ObjectAnimator;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -28,14 +29,15 @@
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.Property;
-import android.util.SparseArray;
 
+import com.android.launcher3.graphics.PlaceHolderIconDrawable;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.util.Themes;
+
 
 public class FastBitmapDrawable extends Drawable {
 
@@ -46,17 +48,7 @@
 
     public static final int CLICK_FEEDBACK_DURATION = 200;
 
-    // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
-    // reduce the value space to a smaller value V, which reduces the number of cached
-    // ColorMatrixColorFilters that we need to keep to V^2
-    private static final int REDUCED_FILTER_VALUE_SPACE = 48;
-
-    // A cache of ColorFilters for optimizing brightness and saturation animations
-    private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
-
-    // Temporary matrices used for calculation
-    private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
-    private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
+    private static ColorFilter sDisabledFColorFilter;
 
     protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
     protected Bitmap mBitmap;
@@ -64,6 +56,7 @@
 
     private boolean mIsPressed;
     private boolean mIsDisabled;
+    private float mDisabledAlpha = 1f;
 
     // Animator and properties for the fast bitmap drawable's scale
     private static final Property<FastBitmapDrawable, Float> SCALE
@@ -82,13 +75,7 @@
     private ObjectAnimator mScaleAnimation;
     private float mScale = 1;
 
-
-    // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
-    // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
-    private int mDesaturation = 0;
-    private int mBrightness = 0;
     private int mAlpha = 255;
-    private int mPrevUpdateKey = Integer.MAX_VALUE;
 
     public FastBitmapDrawable(Bitmap b) {
         this(b, Color.TRANSPARENT);
@@ -98,10 +85,6 @@
         this(info.icon, info.color);
     }
 
-    public FastBitmapDrawable(ItemInfoWithIcon info) {
-        this(info.iconBitmap, info.iconColor);
-    }
-
     protected FastBitmapDrawable(Bitmap b, int iconColor) {
         this(b, iconColor, false);
     }
@@ -245,15 +228,10 @@
         return false;
     }
 
-    private void invalidateDesaturationAndBrightness() {
-        setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0);
-        setBrightness(mIsDisabled ? DISABLED_BRIGHTNESS : 0);
-    }
-
     public void setIsDisabled(boolean isDisabled) {
         if (mIsDisabled != isDisabled) {
             mIsDisabled = isDisabled;
-            invalidateDesaturationAndBrightness();
+            updateFilter();
         }
     }
 
@@ -261,90 +239,33 @@
         return mIsDisabled;
     }
 
-    /**
-     * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
-     */
-    private void setDesaturation(float desaturation) {
-        int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
-        if (mDesaturation != newDesaturation) {
-            mDesaturation = newDesaturation;
-            updateFilter();
+    private ColorFilter getDisabledColorFilter() {
+        if (sDisabledFColorFilter == null) {
+            ColorMatrix tempBrightnessMatrix = new ColorMatrix();
+            ColorMatrix tempFilterMatrix = new ColorMatrix();
+
+            tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
+            float scale = 1 - DISABLED_BRIGHTNESS;
+            int brightnessI =   (int) (255 * DISABLED_BRIGHTNESS);
+            float[] mat = tempBrightnessMatrix.getArray();
+            mat[0] = scale;
+            mat[6] = scale;
+            mat[12] = scale;
+            mat[4] = brightnessI;
+            mat[9] = brightnessI;
+            mat[14] = brightnessI;
+            mat[18] = mDisabledAlpha;
+            tempFilterMatrix.preConcat(tempBrightnessMatrix);
+            sDisabledFColorFilter = new ColorMatrixColorFilter(tempFilterMatrix);
         }
-    }
-
-    public float getDesaturation() {
-        return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
-    }
-
-    /**
-     * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
-     */
-    private void setBrightness(float brightness) {
-        int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
-        if (mBrightness != newBrightness) {
-            mBrightness = newBrightness;
-            updateFilter();
-        }
-    }
-
-    private float getBrightness() {
-        return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
+        return sDisabledFColorFilter;
     }
 
     /**
      * Updates the paint to reflect the current brightness and saturation.
      */
     protected void updateFilter() {
-        boolean usePorterDuffFilter = false;
-        int key = -1;
-        if (mDesaturation > 0) {
-            key = (mDesaturation << 16) | mBrightness;
-        } else if (mBrightness > 0) {
-            // Compose a key with a fully saturated icon if we are just animating brightness
-            key = (1 << 16) | mBrightness;
-
-            // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
-            // icons, so just use a PorterDuff filter when we aren't animating saturation
-            usePorterDuffFilter = true;
-        }
-
-        // Debounce multiple updates on the same frame
-        if (key == mPrevUpdateKey) {
-            return;
-        }
-        mPrevUpdateKey = key;
-
-        if (key != -1) {
-            ColorFilter filter = sCachedFilter.get(key);
-            if (filter == null) {
-                float brightnessF = getBrightness();
-                int brightnessI = (int) (255 * brightnessF);
-                if (usePorterDuffFilter) {
-                    filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
-                            PorterDuff.Mode.SRC_ATOP);
-                } else {
-                    float saturationF = 1f - getDesaturation();
-                    sTempFilterMatrix.setSaturation(saturationF);
-                    if (mBrightness > 0) {
-                        // Brightness: C-new = C-old*(1-amount) + amount
-                        float scale = 1f - brightnessF;
-                        float[] mat = sTempBrightnessMatrix.getArray();
-                        mat[0] = scale;
-                        mat[6] = scale;
-                        mat[12] = scale;
-                        mat[4] = brightnessI;
-                        mat[9] = brightnessI;
-                        mat[14] = brightnessI;
-                        sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
-                    }
-                    filter = new ColorMatrixColorFilter(sTempFilterMatrix);
-                }
-                sCachedFilter.append(key, filter);
-            }
-            mPaint.setColorFilter(filter);
-        } else {
-            mPaint.setColorFilter(null);
-        }
+        mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
         invalidateSelf();
     }
 
@@ -365,7 +286,7 @@
         }
 
         @Override
-        public Drawable newDrawable() {
+        public FastBitmapDrawable newDrawable() {
             return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
         }
 
@@ -374,4 +295,40 @@
             return 0;
         }
     }
+
+    /**
+     * Interface to be implemented by custom {@link BitmapInfo} to handle drawable construction
+     */
+    public interface Factory {
+
+        /**
+         * Called to create a new drawable
+         */
+        FastBitmapDrawable newDrawable();
+    }
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public static FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
+        FastBitmapDrawable drawable = newIcon(context, info.bitmap);
+        drawable.setIsDisabled(info.isDisabled());
+        return drawable;
+    }
+
+    /**
+     * Creates a drawable for the provided BitmapInfo
+     */
+    public static FastBitmapDrawable newIcon(Context context, BitmapInfo info) {
+        final FastBitmapDrawable drawable;
+        if (info instanceof Factory) {
+            drawable = ((Factory) info).newDrawable();
+        } else if (info.isLowRes()) {
+            drawable = new PlaceHolderIconDrawable(info, context);
+        } else {
+            drawable = new FastBitmapDrawable(info);
+        }
+        drawable.mDisabledAlpha = Themes.getFloat(context, R.attr.disabledIconAlpha, 1f);
+        return drawable;
+    }
 }
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 466b7b2..e5aecf7 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderPagedView;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.FocusLogic;
 import com.android.launcher3.util.Thunk;
 
@@ -96,7 +97,7 @@
             }
 
             if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
-                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                if (FeatureFlags.IS_STUDIO_BUILD) {
                     throw new IllegalStateException("Parent of the focused item is not supported.");
                 } else {
                     return false;
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
deleted file mode 100644
index e2b7b68..0000000
--- a/src/com/android/launcher3/FolderInfo.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.os.Process;
-
-import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.util.ContentWriter;
-
-import java.util.ArrayList;
-
-/**
- * Represents a folder containing shortcuts or apps.
- */
-public class FolderInfo extends ItemInfo {
-
-    public static final int NO_FLAGS = 0x00000000;
-
-    /**
-     * The folder is locked in sorted mode
-     */
-    public static final int FLAG_ITEMS_SORTED = 0x00000001;
-
-    /**
-     * It is a work folder
-     */
-    public static final int FLAG_WORK_FOLDER = 0x00000002;
-
-    /**
-     * The multi-page animation has run for this folder
-     */
-    public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
-
-    public int options;
-
-    /**
-     * The apps and shortcuts
-     */
-    public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
-
-    private ArrayList<FolderListener> mListeners = new ArrayList<>();
-
-    public FolderInfo() {
-        itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
-        user = Process.myUserHandle();
-    }
-
-    /**
-     * Add an app or shortcut
-     *
-     * @param item
-     */
-    public void add(WorkspaceItemInfo item, boolean animate) {
-        add(item, contents.size(), animate);
-    }
-
-    /**
-     * Add an app or shortcut for a specified rank.
-     */
-    public void add(WorkspaceItemInfo item, int rank, boolean animate) {
-        rank = Utilities.boundToRange(rank, 0, contents.size() + 1);
-        contents.add(rank, item);
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).onAdd(item, rank);
-        }
-        itemsChanged(animate);
-    }
-
-    /**
-     * Remove an app or shortcut. Does not change the DB.
-     *
-     * @param item
-     */
-    public void remove(WorkspaceItemInfo item, boolean animate) {
-        contents.remove(item);
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).onRemove(item);
-        }
-        itemsChanged(animate);
-    }
-
-    @Override
-    public void onAddToDatabase(ContentWriter writer) {
-        super.onAddToDatabase(writer);
-        writer.put(LauncherSettings.Favorites.TITLE, title)
-                .put(LauncherSettings.Favorites.OPTIONS, options);
-    }
-
-    public void addListener(FolderListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void removeListener(FolderListener listener) {
-        mListeners.remove(listener);
-    }
-
-    public void itemsChanged(boolean animate) {
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).onItemsChanged(animate);
-        }
-    }
-
-    public interface FolderListener {
-        public void onAdd(WorkspaceItemInfo item, int rank);
-        public void onRemove(WorkspaceItemInfo item);
-        public void onItemsChanged(boolean animate);
-    }
-
-    public boolean hasOption(int optionFlag) {
-        return (options & optionFlag) != 0;
-    }
-
-    /**
-     * @param option flag to set or clear
-     * @param isEnabled whether to set or clear the flag
-     * @param writer if not null, save changes to the db.
-     */
-    public void setOption(int option, boolean isEnabled, ModelWriter writer) {
-        int oldOptions = options;
-        if (isEnabled) {
-            options |= option;
-        } else {
-            options &= ~option;
-        }
-        if (writer != null && oldOptions != options) {
-            writer.updateItemInDatabase(this);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 03ee707..51f3819 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,26 +16,30 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+
 import android.content.Context;
 import android.graphics.Rect;
 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;
 
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.views.Transposable;
 
-public class Hotseat extends CellLayout implements LogContainerProvider, Insettable, Transposable {
+import java.util.ArrayList;
+
+public class Hotseat extends CellLayout implements LogContainerProvider, Insettable {
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mHasVerticalHotseat;
+    private Workspace mWorkspace;
+    private boolean mSendTouchToWorkspace;
 
     public Hotseat(Context context) {
         this(context, null);
@@ -49,12 +53,17 @@
         super(context, attrs, defStyle);
     }
 
-    /* Get the orientation specific coordinates given an invariant order in the hotseat. */
-    int getCellXFromOrder(int rank) {
+    /**
+     * Returns orientation specific cell X given invariant order in the hotseat
+     */
+    public int getCellXFromOrder(int rank) {
         return mHasVerticalHotseat ? 0 : rank;
     }
 
-    int getCellYFromOrder(int rank) {
+    /**
+     * Returns orientation specific cell Y given invariant order in the hotseat
+     */
+    public int getCellYFromOrder(int rank) {
         return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0;
     }
 
@@ -70,17 +79,18 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        target.gridX = info.cellX;
-        target.gridY = info.cellY;
-        targetParent.containerType = LauncherLogProto.ContainerType.HOTSEAT;
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        child.rank = childInfo.rank;
+        child.gridX = childInfo.cellX;
+        child.gridY = childInfo.cellY;
+        parents.add(newContainerTarget(LauncherLogProto.ContainerType.HOTSEAT));
     }
 
     @Override
     public void setInsets(Rect insets) {
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
-        DeviceProfile grid = mActivity.getWallpaperDeviceProfile();
-        insets = grid.getInsets();
+        DeviceProfile grid = mActivity.getDeviceProfile();
 
         if (grid.isVerticalBarLayout()) {
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
@@ -103,13 +113,35 @@
         InsettableFrameLayout.dispatchInsets(this, insets);
     }
 
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        return event.getY() > getCellHeight();
+    public void setWorkspace(Workspace w) {
+        mWorkspace = w;
     }
 
     @Override
-    public RotationMode getRotationMode() {
-        return Launcher.getLauncher(getContext()).getRotationMode();
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating
+        // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace
+        // for the remainder of the this input stream.
+        int yThreshold = getMeasuredHeight() - getPaddingBottom();
+        if (mWorkspace != null && ev.getY() <= yThreshold) {
+            mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev);
+            return mSendTouchToWorkspace;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // See comment in #onInterceptTouchEvent
+        if (mSendTouchToWorkspace) {
+            final int action = event.getAction();
+            switch (action & MotionEvent.ACTION_MASK) {
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    mSendTouchToWorkspace = false;
+            }
+            return mWorkspace.onTouchEvent(event);
+        }
+        return event.getY() > getCellHeight();
     }
 }
diff --git a/src/com/android/launcher3/IconProvider.java b/src/com/android/launcher3/IconProvider.java
deleted file mode 100644
index 0f006f7..0000000
--- a/src/com/android/launcher3/IconProvider.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.android.launcher3;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.content.pm.LauncherActivityInfo;
-import android.graphics.drawable.Drawable;
-
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-public class IconProvider implements ResourceBasedOverride {
-
-    public static MainThreadInitializedObject<IconProvider> INSTANCE =
-            forOverride(IconProvider.class, R.string.icon_provider_class);
-
-    public IconProvider() { }
-
-    public String getSystemStateForPackage(String systemState, String packageName) {
-        return systemState;
-    }
-
-    /**
-     * @param flattenDrawable true if the caller does not care about the specification of the
-     *                        original icon as long as the flattened version looks the same.
-     */
-    public Drawable getIcon(LauncherActivityInfo info, int iconDpi, boolean flattenDrawable) {
-        return info.getIcon(iconDpi);
-    }
-}
diff --git a/src/com/android/launcher3/InsettableFrameLayout.java b/src/com/android/launcher3/InsettableFrameLayout.java
index faa18b8..9a66d32 100644
--- a/src/com/android/launcher3/InsettableFrameLayout.java
+++ b/src/com/android/launcher3/InsettableFrameLayout.java
@@ -91,6 +91,9 @@
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
+        if (!isAttachedToWindow()) {
+            return;
+        }
         setFrameLayoutChildInsets(child, mInsets, new Rect());
     }
 
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index fe91602..62c9b4d 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -27,6 +27,7 @@
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Bitmap;
@@ -42,13 +43,16 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+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.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
@@ -59,7 +63,6 @@
 
 import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -71,7 +74,6 @@
     public static final int FLAG_ACTIVITY_PAUSED = 1;
     public static final int FLAG_LOADER_RUNNING = 2;
     public static final int FLAG_DRAG_AND_DROP = 4;
-    public static final int FLAG_BULK_ADD = 4;
 
     // Determines whether to defer installing shortcuts immediately until
     // processAllPendingInstalls() is called.
@@ -112,8 +114,7 @@
 
     @WorkerThread
     private static void flushQueueInBackground(Context context) {
-        LauncherModel model = LauncherAppState.getInstance(context).getModel();
-        if (model.getCallback() == null) {
+        if (Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null) {
             // Launcher not loaded
             return;
         }
@@ -126,7 +127,7 @@
             return;
         }
 
-        LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
+        LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
         for (String encoded : strings) {
             PendingInstallShortcutInfo info = decode(encoded, context);
             if (info == null) {
@@ -135,7 +136,7 @@
 
             String pkg = getIntentPackage(info.launchIntent);
             if (!TextUtils.isEmpty(pkg)
-                    && !launcherApps.isPackageEnabledForProfile(pkg, info.user)
+                    && !launcherApps.isPackageEnabled(pkg, info.user)
                     && !info.isActivity) {
                 if (DBG) {
                     Log.d(TAG, "Ignoring shortcut for absent package: " + info.launchIntent);
@@ -148,7 +149,8 @@
         }
         prefs.edit().remove(APPS_PENDING_INSTALL).apply();
         if (!installQueue.isEmpty()) {
-            model.addAndBindAddedWorkspaceItems(installQueue);
+            LauncherAppState.getInstance(context).getModel()
+                    .addAndBindAddedWorkspaceItems(installQueue);
         }
     }
 
@@ -421,7 +423,7 @@
                             .object()
                             .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                             .key(DEEPSHORTCUT_TYPE_KEY).value(true)
-                            .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext)
+                            .key(USER_HANDLE_KEY).value(UserCache.INSTANCE.get(mContext)
                                     .getSerialNumberForUser(user))
                             .endObject().toString();
                 } else if (providerInfo != null) {
@@ -431,7 +433,7 @@
                             .object()
                             .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                             .key(APP_WIDGET_TYPE_KEY).value(true)
-                            .key(USER_HANDLE_KEY).value(UserManagerCompat.getInstance(mContext)
+                            .key(USER_HANDLE_KEY).value(UserCache.INSTANCE.get(mContext)
                                     .getSerialNumberForUser(user))
                             .endObject().toString();
                 }
@@ -459,7 +461,7 @@
                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                     .key(NAME_KEY).value(name)
                     .key(USER_HANDLE_KEY).value(
-                            UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user))
+                            UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user))
                     .key(APP_SHORTCUT_TYPE_KEY).value(isActivity);
                 if (icon != null) {
                     byte[] iconByteArray = GraphicsUtils.flattenBitmap(icon);
@@ -490,9 +492,8 @@
                 return Pair.create(si, null);
             } else if (shortcutInfo != null) {
                 WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, mContext);
-                LauncherIcons li = LauncherIcons.obtain(mContext);
-                itemInfo.applyFrom(li.createShortcutIcon(shortcutInfo));
-                li.recycle();
+                LauncherAppState.getInstance(mContext).getIconCache().getShortcutIcon(
+                        itemInfo, shortcutInfo);
                 return Pair.create(itemInfo, shortcutInfo);
             } else if (providerInfo != null) {
                 LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo
@@ -527,18 +528,15 @@
         try {
             Decoder decoder = new Decoder(encoded, context);
             if (decoder.optBoolean(APP_SHORTCUT_TYPE_KEY)) {
-                LauncherActivityInfo info = LauncherAppsCompat.getInstance(context)
+                LauncherActivityInfo info = context.getSystemService(LauncherApps.class)
                         .resolveActivity(decoder.launcherIntent, decoder.user);
                 if (info != null) {
                     return new PendingInstallShortcutInfo(info, context);
                 }
             } else if (decoder.optBoolean(DEEPSHORTCUT_TYPE_KEY)) {
-                DeepShortcutManager sm = DeepShortcutManager.getInstance(context);
-                List<ShortcutInfo> si = sm.queryForFullDetails(
-                        decoder.launcherIntent.getPackage(),
-                        Arrays.asList(decoder.launcherIntent.getStringExtra(
-                                ShortcutKey.EXTRA_SHORTCUT_ID)),
-                        decoder.user);
+                List<ShortcutInfo> si = ShortcutKey.fromIntent(decoder.launcherIntent, decoder.user)
+                        .buildRequest(context)
+                        .query(ShortcutRequest.ALL);
                 if (si.isEmpty()) {
                     return null;
                 } else {
@@ -593,7 +591,7 @@
         private Decoder(String encoded, Context context) throws JSONException, URISyntaxException {
             super(encoded);
             launcherIntent = Intent.parseUri(getString(LAUNCH_INTENT_KEY), 0);
-            user = has(USER_HANDLE_KEY) ? UserManagerCompat.getInstance(context)
+            user = has(USER_HANDLE_KEY) ? UserCache.INSTANCE.get(context)
                     .getUserForSerialNumber(getLong(USER_HANDLE_KEY))
                     : Process.myUserHandle();
             if (user == null) {
@@ -617,7 +615,7 @@
             return original;
         }
 
-        LauncherActivityInfo info = LauncherAppsCompat.getInstance(original.mContext)
+        LauncherActivityInfo info = original.mContext.getSystemService(LauncherApps.class)
                 .resolveActivity(original.launchIntent, original.user);
         if (info == null) {
             return original;
@@ -662,7 +660,7 @@
         if (iconInfo == null) {
             iconInfo = app.getIconCache().getDefaultIcon(info.user);
         }
-        info.applyFrom(iconInfo);
+        info.bitmap = iconInfo;
 
         info.title = Utilities.trim(name);
         info.contentDescription = app.getContext().getPackageManager()
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index d66ba73..e39e89c 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -17,9 +17,9 @@
 package com.android.launcher3;
 
 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.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.settings.SettingsActivity.GRID_OPTIONS_PREFERENCE_KEY;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.annotation.TargetApi;
@@ -41,6 +41,7 @@
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.util.Xml;
+import android.view.Display;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -48,6 +49,7 @@
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DefaultDisplay.Info;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Themes;
@@ -58,7 +60,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 
 public class InvariantDeviceProfile {
 
@@ -67,6 +68,9 @@
     public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
             new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
+    public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
+    public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
+
     private static final String KEY_IDP_GRID_NAME = "idp_grid_name";
 
     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
@@ -119,6 +123,7 @@
      */
     public int numAllAppsColumns;
 
+    public String dbFile;
     public int defaultLayoutId;
     int demoModeLayoutId;
 
@@ -143,9 +148,11 @@
         iconSize = p.iconSize;
         iconShapePath = p.iconShapePath;
         landscapeIconSize = p.landscapeIconSize;
+        iconBitmapSize = p.iconBitmapSize;
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
+        dbFile = p.dbFile;
         allAppsIconSize = p.allAppsIconSize;
         allAppsIconTextSize = p.allAppsIconTextSize;
         defaultLayoutId = p.defaultLayoutId;
@@ -156,10 +163,16 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
-        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
-        initGrid(context, gridName);
+        String gridName = getCurrentGridName(context);
+        String newGridName = initGrid(context, gridName);
+        if (!newGridName.equals(gridName)) {
+            Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply();
+        }
+        Utilities.getPrefs(context).edit()
+                .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, numHotseatIcons)
+                .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
+                .apply();
+
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
         mOverlayMonitor = new OverlayMonitor(context);
@@ -176,6 +189,37 @@
     }
 
     /**
+     * This constructor should NOT have any monitors by design.
+     */
+    public InvariantDeviceProfile(Context context, Display display) {
+        // Ensure that the main device profile is initialized
+        InvariantDeviceProfile originalProfile = INSTANCE.get(context);
+        String gridName = getCurrentGridName(context);
+
+        // Get the display info based on default display and interpolate it to existing display
+        DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
+                DefaultDisplay.INSTANCE.get(context).getInfo(),
+                getPredefinedDeviceProfiles(context, gridName));
+
+        Info myInfo = new Info(context, display);
+        DisplayOption myDisplayOption = invDistWeightedInterpolate(
+                myInfo, getPredefinedDeviceProfiles(context, gridName));
+
+        DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
+                .add(myDisplayOption);
+        result.iconSize = defaultDisplayOption.iconSize;
+        result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
+        result.allAppsIconSize = Math.min(
+                defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+        initGrid(context, myInfo, result);
+    }
+
+    public static String getCurrentGridName(Context context) {
+        return Utilities.isGridOptionsEnabled(context)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
+    }
+
+    /**
      * Retrieve system defined or RRO overriden icon shape.
      */
     private static String getIconShapePath(Context context) {
@@ -188,15 +232,46 @@
 
     private String initGrid(Context context, String gridName) {
         DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
+        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
 
-        Point smallestSize = new Point(displayInfo.smallestSize);
-        Point largestSize = new Point(displayInfo.largestSize);
+        DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
+        initGrid(context, displayInfo, displayOption);
+        return displayOption.grid.name;
+    }
 
-        // This guarantees that width < height
-        float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
-                displayInfo.metrics);
-        float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
-                displayInfo.metrics);
+    private void initGrid(
+            Context context, DefaultDisplay.Info displayInfo, DisplayOption displayOption) {
+        GridOption closestProfile = displayOption.grid;
+        numRows = closestProfile.numRows;
+        numColumns = closestProfile.numColumns;
+        numHotseatIcons = closestProfile.numHotseatIcons;
+        dbFile = closestProfile.dbFile;
+        defaultLayoutId = closestProfile.defaultLayoutId;
+        demoModeLayoutId = closestProfile.demoModeLayoutId;
+        numFolderRows = closestProfile.numFolderRows;
+        numFolderColumns = closestProfile.numFolderColumns;
+        numAllAppsColumns = closestProfile.numAllAppsColumns;
+
+        mExtraAttrs = closestProfile.extraAttrs;
+
+        iconSize = displayOption.iconSize;
+        iconShapePath = getIconShapePath(context);
+        landscapeIconSize = displayOption.landscapeIconSize;
+        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
+        iconTextSize = displayOption.iconTextSize;
+        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+
+        if (Utilities.isGridOptionsEnabled(context)) {
+            allAppsIconSize = displayOption.allAppsIconSize;
+            allAppsIconTextSize = displayOption.allAppsIconTextSize;
+        } else {
+            allAppsIconSize = iconSize;
+            allAppsIconTextSize = iconTextSize;
+        }
+
+        // If the partner customization apk contains any grid overrides, apply them
+        // Supported overrides: numRows, numColumns, iconSize
+        applyPartnerDeviceProfileOverrides(context, displayInfo.metrics);
 
         Point realSize = new Point(displayInfo.realSize);
         // The real size never changes. smallSide and largeSide will remain the
@@ -204,64 +279,12 @@
         int smallSide = Math.min(realSize.x, realSize.y);
         int largeSide = Math.max(realSize.x, realSize.y);
 
-        // We want a list of all options as well as the list of filtered options. This allows us
-        // to have a consistent UI for areas that the grid size change should not affect
-        // ie. All Apps should be consistent between grid sizes.
-        ArrayList<DisplayOption> allOptions = new ArrayList<>();
-        ArrayList<DisplayOption> filteredOptions = new ArrayList<>();
-        getPredefinedDeviceProfiles(context, gridName, filteredOptions, allOptions);
+        DeviceProfile.Builder builder = new DeviceProfile.Builder(context, this, displayInfo)
+                .setSizeRange(new Point(displayInfo.smallestSize),
+                        new Point(displayInfo.largestSize));
 
-        if (allOptions.isEmpty() && filteredOptions.isEmpty()) {
-            throw new RuntimeException("No display option with canBeDefault=true");
-        }
-
-        // Sort the profiles based on the closeness to the device size
-        Comparator<DisplayOption> comparator = (a, b) -> Float.compare(dist(minWidthDps,
-                minHeightDps, a.minWidthDps, a.minHeightDps),
-                dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps));
-
-        // Calculate the device profiles as if there is no grid override.
-        Collections.sort(allOptions, comparator);
-        DisplayOption interpolatedDisplayOption =
-                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
-        initGridOption(context, allOptions, interpolatedDisplayOption, displayInfo.metrics);
-
-        // Create IDP with no grid override values.
-        InvariantDeviceProfile originalIDP = new InvariantDeviceProfile(this);
-        originalIDP.landscapeProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, largeSide, smallSide, true /* isLandscape */,
-                false /* isMultiWindowMode */);
-        originalIDP.portraitProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, smallSide, largeSide, false /* isLandscape */,
-                false /* isMultiWindowMode */);
-
-        if (filteredOptions.isEmpty()) {
-            filteredOptions = allOptions;
-
-            landscapeProfile = originalIDP.landscapeProfile;
-            portraitProfile = originalIDP.portraitProfile;
-        } else {
-            Collections.sort(filteredOptions, comparator);
-            interpolatedDisplayOption =
-                    invDistWeightedInterpolate(minWidthDps, minHeightDps, filteredOptions);
-
-            initGridOption(context, filteredOptions, interpolatedDisplayOption,
-                    displayInfo.metrics);
-            numAllAppsColumns = originalIDP.numAllAppsColumns;
-
-            landscapeProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, largeSide, smallSide, true /* isLandscape */,
-                    false /* isMultiWindowMode */);
-            portraitProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, smallSide, largeSide, false /* isLandscape */,
-                    false /* isMultiWindowMode */);
-        }
-
-        GridOption closestProfile = filteredOptions.get(0).grid;
-        if (!closestProfile.name.equals(gridName)) {
-            Utilities.getPrefs(context).edit()
-                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
-        }
+        landscapeProfile = builder.setSize(largeSide, smallSide).build();
+        portraitProfile = builder.setSize(smallSide, largeSide).build();
 
         // We need to ensure that there is enough extra space in the wallpaper
         // for the intended parallax effects
@@ -275,37 +298,8 @@
 
         ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
         defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
-
-        return closestProfile.name;
     }
 
-    private void initGridOption(Context context, ArrayList<DisplayOption> options,
-            DisplayOption displayOption, DisplayMetrics metrics) {
-        GridOption closestProfile = options.get(0).grid;
-        numRows = closestProfile.numRows;
-        numColumns = closestProfile.numColumns;
-        numHotseatIcons = closestProfile.numHotseatIcons;
-        defaultLayoutId = closestProfile.defaultLayoutId;
-        demoModeLayoutId = closestProfile.demoModeLayoutId;
-        numFolderRows = closestProfile.numFolderRows;
-        numFolderColumns = closestProfile.numFolderColumns;
-        numAllAppsColumns = numColumns;
-
-        mExtraAttrs = closestProfile.extraAttrs;
-
-        iconSize = displayOption.iconSize;
-        iconShapePath = getIconShapePath(context);
-        landscapeIconSize = displayOption.landscapeIconSize;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
-        iconTextSize = displayOption.iconTextSize;
-        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
-
-        // If the partner customization apk contains any grid overrides, apply them
-        // Supported overrides: numRows, numColumns, iconSize
-        applyPartnerDeviceProfileOverrides(context, metrics);
-    }
-
-
     @Nullable
     public TypedValue getAttrValue(int attr) {
         return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
@@ -348,9 +342,7 @@
         InvariantDeviceProfile oldProfile = new InvariantDeviceProfile(this);
 
         // Re-init grid
-        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
+        String gridName = getCurrentGridName(context);
         initGrid(context, gridName);
 
         int changeFlags = 0;
@@ -383,13 +375,7 @@
         }
     }
 
-    /**
-     * @param gridName The current grid name.
-     * @param filteredOptionsOut List filled with all the filtered options based on gridName.
-     * @param allOptionsOut List filled with all the options that can be the default option.
-     */
-    static void getPredefinedDeviceProfiles(Context context, String gridName,
-            ArrayList<DisplayOption> filteredOptionsOut, ArrayList<DisplayOption> allOptionsOut) {
+    static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
@@ -416,19 +402,26 @@
             throw new RuntimeException(e);
         }
 
+        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
                 if (gridName.equals(option.grid.name)) {
-                    filteredOptionsOut.add(option);
+                    filteredProfiles.add(option);
                 }
             }
         }
-
-        for (DisplayOption option : profiles) {
-            if (option.canBeDefault) {
-                allOptionsOut.add(option);
+        if (filteredProfiles.isEmpty()) {
+            // No grid found, use the default options
+            for (DisplayOption option : profiles) {
+                if (option.canBeDefault) {
+                    filteredProfiles.add(option);
+                }
             }
         }
+        if (filteredProfiles.isEmpty()) {
+            throw new RuntimeException("No display option with canBeDefault=true");
+        }
+        return filteredProfiles;
     }
 
     private int getLauncherIconDensity(int requiredSize) {
@@ -472,8 +465,43 @@
     }
 
     @VisibleForTesting
+    static DisplayOption invDistWeightedInterpolate(
+            DefaultDisplay.Info displayInfo, ArrayList<DisplayOption> points) {
+        Point smallestSize = new Point(displayInfo.smallestSize);
+        Point largestSize = new Point(displayInfo.largestSize);
+
+        // This guarantees that width < height
+        float width = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
+                displayInfo.metrics);
+        float height = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
+                displayInfo.metrics);
+
+        // Sort the profiles based on the closeness to the device size
+        Collections.sort(points, (a, b) ->
+                Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
+                        dist(width, height, b.minWidthDps, b.minHeightDps)));
+
+        GridOption closestOption = points.get(0).grid;
+        float weights = 0;
+
+        DisplayOption p = points.get(0);
+        if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
+            return p;
+        }
+
+        DisplayOption out = new DisplayOption(closestOption);
+        for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
+            p = points.get(i);
+            float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
+            weights += w;
+            out.add(new DisplayOption().add(p).multiply(w));
+        }
+        return out.multiply(1.0f / weights);
+    }
+
+    @VisibleForTesting
     static DisplayOption invDistWeightedInterpolate(float width, float height,
-                ArrayList<DisplayOption> points) {
+            ArrayList<DisplayOption> points) {
         float weights = 0;
 
         DisplayOption p = points.get(0);
@@ -552,6 +580,9 @@
 
         private final int numHotseatIcons;
 
+        private final String dbFile;
+        private final int numAllAppsColumns;
+
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
@@ -564,6 +595,7 @@
             numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
             numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
 
+            dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
             defaultLayoutId = a.getResourceId(
                     R.styleable.GridDisplayOption_defaultLayoutId, 0);
             demoModeLayoutId = a.getResourceId(
@@ -574,6 +606,8 @@
                     R.styleable.GridDisplayOption_numFolderRows, numRows);
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+            numAllAppsColumns = a.getInt(
+                    R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
             a.recycle();
 
@@ -585,7 +619,6 @@
     private static final class DisplayOption {
         private final GridOption grid;
 
-        private final String name;
         private final float minWidthDps;
         private final float minHeightDps;
         private final boolean canBeDefault;
@@ -593,6 +626,8 @@
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
+        private float allAppsIconSize;
+        private float allAppsIconTextSize;
 
         DisplayOption(GridOption grid, Context context, AttributeSet attrs) {
             this.grid = grid;
@@ -600,7 +635,6 @@
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.ProfileDisplayOption);
 
-            name = a.getString(R.styleable.ProfileDisplayOption_name);
             minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0);
             minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0);
             canBeDefault = a.getBoolean(
@@ -611,12 +645,19 @@
                     iconSize);
             iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0);
 
+            allAppsIconSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize,
+                    iconSize);
+            allAppsIconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize,
+                    iconTextSize);
             a.recycle();
         }
 
         DisplayOption() {
-            grid = null;
-            name = null;
+            this(null);
+        }
+
+        DisplayOption(GridOption grid) {
+            this.grid = grid;
             minWidthDps = 0;
             minHeightDps = 0;
             canBeDefault = false;
@@ -625,14 +666,18 @@
         private DisplayOption multiply(float w) {
             iconSize *= w;
             landscapeIconSize *= w;
+            allAppsIconSize *= w;
             iconTextSize *= w;
+            allAppsIconTextSize *= w;
             return this;
         }
 
         private DisplayOption add(DisplayOption p) {
             iconSize += p.iconSize;
             landscapeIconSize += p.landscapeIconSize;
+            allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
+            allAppsIconTextSize += p.allAppsIconTextSize;
             return this;
         }
     }
@@ -650,4 +695,4 @@
             onConfigChanged(context);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
deleted file mode 100644
index 3f723d1..0000000
--- a/src/com/android/launcher3/ItemInfo.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.content.ComponentName;
-import android.content.ContentValues;
-import android.content.Intent;
-import android.os.Process;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.util.ContentWriter;
-
-/**
- * Represents an item in the launcher.
- */
-public class ItemInfo {
-
-    public static final int NO_ID = -1;
-
-    /**
-     * The id in the settings database for this item
-     */
-    public int id = NO_ID;
-
-    /**
-     * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_FOLDER},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPWIDGET} or
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_CUSTOM_APPWIDGET}.
-     */
-    public int itemType;
-
-    /**
-     * The id of the container that holds this item. For the desktop, this will be
-     * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. For the all applications folder it
-     * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders
-     * it will be the id of the folder.
-     */
-    public int container = NO_ID;
-
-    /**
-     * Indicates the screen in which the shortcut appears if the container types is
-     * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. (i.e., ignore if the container type is
-     * {@link LauncherSettings.Favorites#CONTAINER_HOTSEAT})
-     */
-    public int screenId = -1;
-
-    /**
-     * Indicates the X position of the associated cell.
-     */
-    public int cellX = -1;
-
-    /**
-     * Indicates the Y position of the associated cell.
-     */
-    public int cellY = -1;
-
-    /**
-     * Indicates the X cell span.
-     */
-    public int spanX = 1;
-
-    /**
-     * Indicates the Y cell span.
-     */
-    public int spanY = 1;
-
-    /**
-     * Indicates the minimum X cell span.
-     */
-    public int minSpanX = 1;
-
-    /**
-     * Indicates the minimum Y cell span.
-     */
-    public int minSpanY = 1;
-
-    /**
-     * Indicates the position in an ordered list.
-     */
-    public int rank = 0;
-
-    /**
-     * Title of the item
-     */
-    public CharSequence title;
-
-    /**
-     * Content description of the item.
-     */
-    public CharSequence contentDescription;
-
-    public UserHandle user;
-
-    public ItemInfo() {
-        user = Process.myUserHandle();
-    }
-
-    ItemInfo(ItemInfo info) {
-        copyFrom(info);
-    }
-
-    public void copyFrom(ItemInfo info) {
-        id = info.id;
-        cellX = info.cellX;
-        cellY = info.cellY;
-        spanX = info.spanX;
-        spanY = info.spanY;
-        rank = info.rank;
-        screenId = info.screenId;
-        itemType = info.itemType;
-        container = info.container;
-        user = info.user;
-        contentDescription = info.contentDescription;
-    }
-
-    public Intent getIntent() {
-        return null;
-    }
-
-    @Nullable
-    public ComponentName getTargetComponent() {
-        Intent intent = getIntent();
-        if (intent != null) {
-            return intent.getComponent();
-        } else {
-            return null;
-        }
-    }
-
-    public void writeToValues(ContentWriter writer) {
-        writer.put(LauncherSettings.Favorites.ITEM_TYPE, itemType)
-                .put(LauncherSettings.Favorites.CONTAINER, container)
-                .put(LauncherSettings.Favorites.SCREEN, screenId)
-                .put(LauncherSettings.Favorites.CELLX, cellX)
-                .put(LauncherSettings.Favorites.CELLY, cellY)
-                .put(LauncherSettings.Favorites.SPANX, spanX)
-                .put(LauncherSettings.Favorites.SPANY, spanY)
-                .put(LauncherSettings.Favorites.RANK, rank);
-    }
-
-    public void readFromValues(ContentValues values) {
-        itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
-        container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
-        screenId = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
-        cellX = values.getAsInteger(LauncherSettings.Favorites.CELLX);
-        cellY = values.getAsInteger(LauncherSettings.Favorites.CELLY);
-        spanX = values.getAsInteger(LauncherSettings.Favorites.SPANX);
-        spanY = values.getAsInteger(LauncherSettings.Favorites.SPANY);
-        rank = values.getAsInteger(LauncherSettings.Favorites.RANK);
-    }
-
-    /**
-     * Write the fields of this item to the DB
-     */
-    public void onAddToDatabase(ContentWriter writer) {
-        if (screenId == Workspace.EXTRA_EMPTY_SCREEN_ID) {
-            // We should never persist an item on the extra empty screen.
-            throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID");
-        }
-
-        writeToValues(writer);
-        writer.put(LauncherSettings.Favorites.PROFILE_ID, user);
-    }
-
-    @Override
-    public final String toString() {
-        return getClass().getSimpleName() + "(" + dumpProperties() + ")";
-    }
-
-    protected String dumpProperties() {
-        return "id=" + id
-                + " type=" + LauncherSettings.Favorites.itemTypeToString(itemType)
-                + " container=" + LauncherSettings.Favorites.containerToString((int)container)
-                + " screen=" + screenId
-                + " cell(" + cellX + "," + cellY + ")"
-                + " span(" + spanX + "," + spanY + ")"
-                + " minSpan(" + minSpanX + "," + minSpanY + ")"
-                + " rank=" + rank
-                + " user=" + user
-                + " title=" + title;
-    }
-
-    /**
-     * Whether this item is disabled.
-     */
-    public boolean isDisabled() {
-        return false;
-    }
-
-    public int getViewId() {
-        // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
-        // This cast is safe as long as the id < 0x00FFFFFF
-        // Since we jail all the dynamically generated views, there should be no clashes
-        // with any other views.
-        return id;
-    }
-
-}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 1338b30..0970dae 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -18,21 +18,35 @@
 
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
+import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
+import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
+import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.NO_OFFSET;
+import static com.android.launcher3.LauncherState.NO_SCALE;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
+import static com.android.launcher3.LauncherState.SPRING_LOADED;
+import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newTarget;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+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.logging.StatsLogManager.containerTypeToAtomState;
+import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.INSTALL;
+import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
+import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
-import static com.android.launcher3.util.RaceConditionTracker.ENTER;
-import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -54,11 +68,9 @@
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Point;
-import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
+import android.os.CancellationSignal;
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.StrictMode;
@@ -66,19 +78,21 @@
 import android.text.method.TextKeyListener;
 import android.util.Log;
 import android.util.SparseArray;
-import android.view.Display;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
 import android.view.KeyboardShortcutInfo;
 import android.view.LayoutInflater;
 import android.view.Menu;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.OvershootInterpolator;
 import android.widget.Toast;
 
+import androidx.annotation.CallSuper;
 import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
@@ -88,53 +102,68 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.LauncherAppsCompatVO;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.folder.FolderNameProvider;
-import com.android.launcher3.graphics.RotationMode;
 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.StatsLogUtils;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.logging.UserEventDispatcher.UserEventDelegate;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelWriter;
+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.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.pm.PinRequestHelper;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.qsb.QsbContainerView;
-import com.android.launcher3.states.InternalStateHandler;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.uioverrides.UiFactory;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ActivityResultInfo;
+import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
-import com.android.launcher3.util.RaceConditionTracker;
+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;
+import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
@@ -148,8 +177,15 @@
 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.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.shared.LauncherExterns;
+import com.android.systemui.plugins.shared.LauncherOverlayManager;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -159,14 +195,18 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 /**
  * Default launcher application.
  */
-public class Launcher extends BaseDraggingActivity implements LauncherExterns,
-        Callbacks, LauncherProviderChangeListener, UserEventDelegate,
-        InvariantDeviceProfile.OnIDPChangeListener {
+public class Launcher extends StatefulActivity<LauncherState> implements LauncherExterns,
+        Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin> {
     public static final String TAG = "Launcher";
+
+    public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
+
     static final boolean LOGD = false;
 
     static final boolean DEBUG_STRICT_MODE = false;
@@ -202,11 +242,13 @@
     private static final String RUNTIME_STATE_PENDING_ACTIVITY_RESULT = "launcher.activity_result";
     // Type: SparseArray<Parcelable>
     private static final String RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel";
-    public static final String ON_CREATE_EVT = "Launcher.onCreate";
-    private static final String ON_START_EVT = "Launcher.onStart";
-    private static final String ON_RESUME_EVT = "Launcher.onResume";
 
-    private LauncherStateManager mStateManager;
+    public static final String ON_CREATE_EVT = "Launcher.onCreate";
+    public static final String ON_START_EVT = "Launcher.onStart";
+    public static final String ON_RESUME_EVT = "Launcher.onResume";
+    public static final String ON_NEW_INTENT_EVT = "Launcher.onNewIntent";
+
+    private StateManager<LauncherState> mStateManager;
 
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
 
@@ -223,12 +265,11 @@
 
     @Thunk
     Workspace mWorkspace;
-    private View mLauncherView;
     @Thunk
     DragLayer mDragLayer;
     private DragController mDragController;
 
-    private AppWidgetManagerCompat mAppWidgetManager;
+    private WidgetManagerHelper mAppWidgetManager;
     private LauncherAppWidgetHost mAppWidgetHost;
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
@@ -255,6 +296,10 @@
 
     private ArrayList<OnResumeCallback> mOnResumeCallbacks = new ArrayList<>();
 
+    // Used to notify when an activity launch has been deferred because launcher is not yet resumed
+    // TODO: See if we can remove this later
+    private Runnable mOnDeferredActivityLaunchCallback;
+
     private ViewOnDrawExecutor mPendingExecutor;
 
     private LauncherModel mModel;
@@ -265,10 +310,12 @@
     private PopupDataProvider mPopupDataProvider;
 
     private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
+    private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
     private SharedPreferences mSharedPrefs;
+    private OnboardingPrefs mOnboardingPrefs;
 
     // Activity result which needs to be processed after workspace has loaded.
     private ActivityResultInfo mPendingActivityResult;
@@ -278,25 +325,28 @@
      */
     private PendingRequestArgs mPendingRequestArgs;
     // Request id for any pending activity result
-    private int mPendingActivityRequestCode = -1;
+    protected int mPendingActivityRequestCode = -1;
 
-    public ViewGroupFocusHelper mFocusHandler;
+    private ViewGroupFocusHelper mFocusHandler;
 
     private RotationHelper mRotationHelper;
-    private Runnable mCancelTouchController;
-
-    final Handler mHandler = new Handler();
-    private final Runnable mHandleDeferredResume = this::handleDeferredResume;
-    private boolean mDeferredResumePending;
 
     private float mCurrentAssistantVisibility = 0f;
 
-    private DeviceProfile mStableDeviceProfile;
-    private RotationMode mRotationMode = RotationMode.NORMAL;
+    protected LauncherOverlayManager mOverlayManager;
+    // If true, overlay callbacks are deferred
+    private boolean mDeferOverlayCallbacks;
+    private final Runnable mDeferredOverlayCallbacks = this::checkIfOverlayStillDeferred;
+
+    private long mLastTouchUpTime = -1;
+    private boolean mTouchInProgress;
+
+    private SafeCloseable mUserChangedCallbackCloseable;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        RaceConditionTracker.onEvent(ON_CREATE_EVT, ENTER);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         if (DEBUG_STRICT_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
@@ -311,14 +361,13 @@
                     .penaltyDeath()
                     .build());
         }
-        TraceHelper.beginSection("Launcher-onCreate");
 
         super.onCreate(savedInstanceState);
-        TraceHelper.partitionSection("Launcher-onCreate", "super call");
 
         LauncherAppState app = LauncherAppState.getInstance(this);
         mOldConfig = new Configuration(getResources().getConfiguration());
-        mModel = app.setLauncher(this);
+        mModel = app.getModel();
+
         mRotationHelper = new RotationHelper(this);
         InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
         initDeviceProfile(idp);
@@ -329,22 +378,23 @@
 
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
-        mStateManager = new LauncherStateManager(this);
-        UiFactory.onCreate(this);
+        mStateManager = new StateManager<>(this, NORMAL);
 
-        mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
+        mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
+
+        mAppWidgetManager = new WidgetManagerHelper(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this,
                 appWidgetId -> getWorkspace().removeWidget(appWidgetId));
         mAppWidgetHost.startListening();
 
-        mLauncherView = LayoutInflater.from(this).inflate(R.layout.launcher, null);
-
+        inflateRootView(R.layout.launcher);
         setupViews();
-        mPopupDataProvider = new PopupDataProvider(this);
+        mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
 
         mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
+        mAppTransitionManager.registerRemoteAnimations();
 
-        boolean internalStateHandled = InternalStateHandler.handleCreate(this, getIntent());
+        boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
             if (savedInstanceState != null) {
                 // InternalStateHandler has already set the appropriate state.
@@ -357,28 +407,24 @@
 
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
-        int currentScreen = PagedView.INVALID_RESTORE_PAGE;
+        int currentScreen = PagedView.INVALID_PAGE;
         if (savedInstanceState != null) {
             currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
         }
+        mPageToBindSynchronously = currentScreen;
 
-        if (!mModel.startLoader(currentScreen)) {
+        if (!mModel.addCallbacksAndLoad(this)) {
             if (!internalStateHandled) {
                 // If we are not binding synchronously, show a fade in animation when
                 // the first page bind completes.
                 mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
             }
-        } else {
-            // Pages bound synchronously.
-            mWorkspace.setCurrentPage(currentScreen);
-
-            setWorkspaceLoading(true);
         }
 
         // For handling default keys
         setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
 
-        setContentView(mLauncherView);
+        setContentView(getRootView());
         getRootView().dispatchInsets();
 
         // Listen for broadcasts
@@ -390,14 +436,13 @@
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onCreate(savedInstanceState);
         }
+        mOverlayManager = getDefaultOverlay();
+        PluginManagerWrapper.INSTANCE.get(this).addPluginListener(this,
+                OverlayPlugin.class, false /* allowedMultiple */);
+
         mRotationHelper.initialize();
 
-        TraceHelper.endSection("Launcher-onCreate");
-        RaceConditionTracker.onEvent(ON_CREATE_EVT, EXIT);
-        mStateManager.addStateListener(new LauncherStateManager.StateListener() {
-            @Override
-            public void onStateTransitionStart(LauncherState toState) {
-            }
+        mStateManager.addStateListener(new StateListener<LauncherState>() {
 
             @Override
             public void onStateTransitionComplete(LauncherState finalState) {
@@ -413,13 +458,56 @@
                 }
             }
         });
+
+        TraceHelper.INSTANCE.endSection(traceToken);
+
+        mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
+                () -> getStateManager().goToState(NORMAL));
+    }
+
+    protected LauncherOverlayManager getDefaultOverlay() {
+        return new LauncherOverlayManager() { };
+    }
+
+    protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
+        return new OnboardingPrefs<>(this, sharedPrefs);
+    }
+
+    public OnboardingPrefs getOnboardingPrefs() {
+        return mOnboardingPrefs;
+    }
+
+    @Override
+    public void onPluginConnected(OverlayPlugin overlayManager, Context context) {
+        switchOverlay(() -> overlayManager.createOverlayManager(this, this));
+    }
+
+    @Override
+    public void onPluginDisconnected(OverlayPlugin plugin) {
+        switchOverlay(this::getDefaultOverlay);
+    }
+
+    private void switchOverlay(Supplier<LauncherOverlayManager> overlaySupplier) {
+        if (mOverlayManager != null) {
+            mOverlayManager.onActivityDestroyed(this);
+        }
+        mOverlayManager = overlaySupplier.get();
+        if (getRootView().isAttachedToWindow()) {
+            mOverlayManager.onAttachedToWindow();
+        }
+        mDeferOverlayCallbacks = true;
+        checkIfOverlayStillDeferred();
+    }
+
+    @Override
+    protected void dispatchDeviceProfileChanged() {
+        super.dispatchDeviceProfileChanged();
+        mOverlayManager.onDeviceProvideChanged();
     }
 
     @Override
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
-        UiFactory.onEnterAnimationComplete(this);
-        mAllAppsController.highlightWorkTabIfNecessary();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
     }
 
@@ -432,41 +520,9 @@
         }
 
         mOldConfig.setTo(newConfig);
-        UiFactory.onLauncherStateOrResumeChanged(this);
         super.onConfigurationChanged(newConfig);
     }
 
-    public void reload() {
-        onIdpChanged(mDeviceProfile.inv);
-    }
-
-    private boolean supportsFakeLandscapeUI() {
-        return FeatureFlags.FAKE_LANDSCAPE_UI.get() && !mRotationHelper.homeScreenCanRotate();
-    }
-
-    @Override
-    public void reapplyUi() {
-        reapplyUi(true /* cancelCurrentAnimation */);
-    }
-
-    public void reapplyUi(boolean cancelCurrentAnimation) {
-        if (supportsFakeLandscapeUI()) {
-            mRotationMode = mStableDeviceProfile == null
-                    ? RotationMode.NORMAL : UiFactory.getRotationMode(mDeviceProfile);
-        }
-        getRootView().dispatchInsets();
-        getStateManager().reapplyState(cancelCurrentAnimation);
-    }
-
-    @Override
-    public void rebindModel() {
-        int currentPage = mWorkspace.getNextPage();
-        if (mModel.startLoader(currentPage)) {
-            mWorkspace.setCurrentPage(currentPage);
-            setWorkspaceLoading(true);
-        }
-    }
-
     @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
         onIdpChanged(idp);
@@ -475,7 +531,6 @@
     private void onIdpChanged(InvariantDeviceProfile idp) {
         mUserEventDispatcher = null;
 
-        DeviceProfile oldWallpaperProfile = getWallpaperDeviceProfile();
         initDeviceProfile(idp);
         dispatchDeviceProfileChanged();
         reapplyUi();
@@ -484,9 +539,7 @@
         // Calling onSaveInstanceState ensures that static cache used by listWidgets is
         // initialized properly.
         onSaveInstanceState(new Bundle());
-        if (oldWallpaperProfile != getWallpaperDeviceProfile()) {
-            rebindModel();
-        }
+        mModel.rebindCallbacks();
     }
 
     public void onAssistantVisibilityChanged(float visibility) {
@@ -505,80 +558,33 @@
         // Load configuration-specific DeviceProfile
         mDeviceProfile = idp.getDeviceProfile(this);
         if (isInMultiWindowMode()) {
-            // Note: Calls to getSize() can't rely on our cached DefaultDisplay since it can return
-            // the app window size
-            Display display = getWindowManager().getDefaultDisplay();
-            Point mwSize = new Point();
-            display.getSize(mwSize);
-            mDeviceProfile = mDeviceProfile.getMultiWindowProfile(this, mwSize);
+            mDeviceProfile = mDeviceProfile.getMultiWindowProfile(
+                    this, getMultiWindowDisplaySize());
         }
 
-        if (supportsFakeLandscapeUI() && mDeviceProfile.isVerticalBarLayout()) {
-            mStableDeviceProfile = mDeviceProfile.inv.portraitProfile;
-            mRotationMode = UiFactory.getRotationMode(mDeviceProfile);
-        } else {
-            mStableDeviceProfile = null;
-            mRotationMode = RotationMode.NORMAL;
-        }
-
-        mRotationHelper.updateRotationAnimation();
         onDeviceProfileInitiated();
-        mModelWriter = mModel.getWriter(getWallpaperDeviceProfile().isVerticalBarLayout(), true);
-    }
-
-    public void updateInsets(Rect insets) {
-        mDeviceProfile.updateInsets(insets);
-        if (mStableDeviceProfile != null) {
-            Rect r = mStableDeviceProfile.getInsets();
-            mRotationMode.mapInsets(this, insets, r);
-            mStableDeviceProfile.updateInsets(r);
-        }
-    }
-
-    @Override
-    public RotationMode getRotationMode() {
-        return mRotationMode;
-    }
-
-    /**
-     * Device profile to be used by UI elements which are shown directly on top of the wallpaper
-     * and whose presentation is tied to the wallpaper (and physical device) and not the activity
-     * configuration.
-     */
-    @Override
-    public DeviceProfile getWallpaperDeviceProfile() {
-        return mStableDeviceProfile == null ? mDeviceProfile : mStableDeviceProfile;
+        mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true);
     }
 
     public RotationHelper getRotationHelper() {
         return mRotationHelper;
     }
 
-    public LauncherStateManager getStateManager() {
+    public ViewGroupFocusHelper getFocusHandler() {
+        return mFocusHandler;
+    }
+
+    @Override
+    public StateManager<LauncherState> getStateManager() {
         return mStateManager;
     }
 
-    public FolderNameProvider getFolderNameProvider() {
-        return new FolderNameProvider();
-    }
-
-    @Override
-    public <T extends View> T findViewById(int id) {
-        return mLauncherView.findViewById(id);
-    }
-
-    @Override
-    public void onAppWidgetHostReset() {
-        if (mAppWidgetHost != null) {
-            mAppWidgetHost.startListening();
-        }
-    }
-
     private LauncherCallbacks mLauncherCallbacks;
 
     /**
      * Call this after onCreate to set or clear overlay.
      */
+    @Override
     public void setLauncherOverlay(LauncherOverlay overlay) {
         if (overlay != null) {
             overlay.setOverlayCallbacks(new LauncherOverlayCallbacksImpl());
@@ -586,18 +592,16 @@
         mWorkspace.setLauncherOverlay(overlay);
     }
 
+    @Override
+    public void runOnOverlayHidden(Runnable runnable) {
+        getWorkspace().runOnOverlayHidden(runnable);
+    }
+
     public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
         mLauncherCallbacks = callbacks;
         return true;
     }
 
-    @Override
-    public void onLauncherProviderChanged() {
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onLauncherProviderChange();
-        }
-    }
-
     public boolean isDraggingEnabled() {
         // We prevent dragging when we are loading the workspace as it is possible to pick up a view
         // that is subsequently removed from the workspace in startBinding().
@@ -643,6 +647,7 @@
         switch (requestCode) {
             case REQUEST_CREATE_SHORTCUT:
                 completeAddShortcut(intent, info.container, screenId, info.cellX, info.cellY, info);
+                announceForAccessibility(R.string.item_added_to_workspace);
                 break;
             case REQUEST_CREATE_APPWIDGET:
                 completeAddAppWidget(appWidgetId, info, null, null);
@@ -667,7 +672,6 @@
                 break;
             }
         }
-
         return screenId;
     }
 
@@ -702,8 +706,8 @@
                     data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1;
             if (resultCode == RESULT_CANCELED) {
                 completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId, requestArgs);
-                mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded,
-                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false);
+                mWorkspace.removeExtraEmptyScreenDelayed(
+                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false, exitSpringLoaded);
             } else if (resultCode == RESULT_OK) {
                 addAppWidgetImpl(
                         appWidgetId, requestArgs, null,
@@ -733,15 +737,9 @@
                         "returned from the widget configuration activity.");
                 result = RESULT_CANCELED;
                 completeTwoStageWidgetDrop(result, appWidgetId, requestArgs);
-                final Runnable onComplete = new Runnable() {
-                    @Override
-                    public void run() {
-                        getStateManager().goToState(NORMAL);
-                    }
-                };
-
-                mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete,
-                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false);
+                mWorkspace.removeExtraEmptyScreenDelayed(
+                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false,
+                        () -> getStateManager().goToState(NORMAL));
             } else {
                 if (requestArgs.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                     // When the screen id represents an actual screen (as opposed to a rank)
@@ -760,8 +758,8 @@
                         dropLayout.setDropPending(false);
                     }
                 };
-                mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete,
-                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false);
+                mWorkspace.removeExtraEmptyScreenDelayed(
+                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false, onComplete);
             }
             return;
         }
@@ -780,14 +778,15 @@
             // Handle custom shortcuts created using ACTION_CREATE_SHORTCUT.
             if (resultCode == RESULT_OK && requestArgs.container != ItemInfo.NO_ID) {
                 completeAdd(requestCode, data, -1, requestArgs);
-                mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded,
-                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false);
+                mWorkspace.removeExtraEmptyScreenDelayed(
+                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false, exitSpringLoaded);
 
             } else if (resultCode == RESULT_CANCELED) {
-                mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded,
-                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false);
+                mWorkspace.removeExtraEmptyScreenDelayed(
+                        ON_ACTIVITY_RESULT_ANIMATION_DELAY, false, exitSpringLoaded);
             }
         }
+
         mDragLayer.clearAnimatedView();
     }
 
@@ -796,9 +795,6 @@
             final int requestCode, final int resultCode, final Intent data) {
         mPendingActivityRequestCode = -1;
         handleActivityResult(requestCode, resultCode, data);
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onActivityResult(requestCode, resultCode, data);
-        }
     }
 
     @Override
@@ -825,10 +821,6 @@
                         getString(R.string.derived_app_name)), Toast.LENGTH_SHORT).show();
             }
         }
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onRequestPermissionsResult(requestCode, permissions,
-                    grantResults);
-        }
     }
 
     /**
@@ -886,98 +878,170 @@
     @Override
     protected void onStop() {
         super.onStop();
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onStop();
+        if (mDeferOverlayCallbacks) {
+            checkIfOverlayStillDeferred();
+        } else {
+            mOverlayManager.onActivityStopped(this);
         }
+
         logStopAndResume(Action.Command.STOP);
-
         mAppWidgetHost.setListenIfResumed(false);
-
         NotificationListener.removeNotificationsChangedListener();
-        getStateManager().moveToRestState();
-
-        UiFactory.onLauncherStateOrResumeChanged(this);
-
-        // Workaround for b/78520668, explicitly trim memory once UI is hidden
-        onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
     }
 
     @Override
     protected void onStart() {
-        RaceConditionTracker.onEvent(ON_START_EVT, ENTER);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_START_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         super.onStart();
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onStart();
+        if (!mDeferOverlayCallbacks) {
+            mOverlayManager.onActivityStarted(this);
         }
+
         mAppWidgetHost.setListenIfResumed(true);
-        RaceConditionTracker.onEvent(ON_START_EVT, EXIT);
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
-    private void handleDeferredResume() {
-        if (hasBeenResumed() && !mStateManager.getState().disableInteraction) {
-            logStopAndResume(Action.Command.RESUME);
-            getUserEventDispatcher().startSession();
+    @Override
+    @CallSuper
+    protected void onDeferredResumed() {
+        logStopAndResume(Action.Command.RESUME);
+        getUserEventDispatcher().startSession();
 
-            UiFactory.onLauncherStateOrResumeChanged(this);
-            AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
+        AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
 
-            // Process any items that were added while Launcher was away.
-            InstallShortcutReceiver.disableAndFlushInstallQueue(
-                    InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED, this);
+        // Process any items that were added while Launcher was away.
+        InstallShortcutReceiver.disableAndFlushInstallQueue(
+                InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED, this);
 
-            // Refresh shortcuts if the permission changed.
-            mModel.refreshShortcutsIfRequired();
+        // Refresh shortcuts if the permission changed.
+        mModel.refreshShortcutsIfRequired();
 
-            // Set the notification listener and fetch updated notifications when we resume
-            NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+        // Set the notification listener and fetch updated notifications when we resume
+        NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
 
-            DiscoveryBounce.showForHomeIfNeeded(this);
-
-            if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
-                UiFactory.resetPendingActivityResults(this, mPendingActivityRequestCode);
-            }
-            mDeferredResumePending = false;
-        } else {
-            mDeferredResumePending = true;
-        }
+        DiscoveryBounce.showForHomeIfNeeded(this);
     }
 
+
     private void logStopAndResume(int command) {
+        int pageIndex = mWorkspace.isOverlayShown() ? -1 : mWorkspace.getCurrentPage();
         int containerType = mStateManager.getState().containerType;
+
+        StatsLogManager.EventEnum event;
+        StatsLogManager.StatsLogger logger = getStatsLogManager().logger();
+        if (command == Action.Command.RESUME) {
+            logger.withSrcState(LAUNCHER_STATE_BACKGROUND)
+                .withDstState(containerTypeToAtomState(mStateManager.getState().containerType));
+            event = LAUNCHER_ONRESUME;
+        } else { /* command == Action.Command.STOP */
+            logger.withSrcState(containerTypeToAtomState(mStateManager.getState().containerType))
+                    .withDstState(LAUNCHER_STATE_BACKGROUND);
+            event = LAUNCHER_ONSTOP;
+        }
+
         if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
             getUserEventDispatcher().logActionCommand(command,
-                    containerType, -1, mWorkspace.isOverlayShown() ? -1 : 0);
+                    containerType, -1, pageIndex);
+            logger.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                    .setWorkspace(
+                            LauncherAtom.WorkspaceContainer.newBuilder()
+                                    .setPageIndex(pageIndex)).build());
         } else {
             getUserEventDispatcher().logActionCommand(command, containerType, -1);
         }
-
+        logger.log(event);
     }
 
+    private void scheduleDeferredCheck() {
+        mHandler.removeCallbacks(mDeferredOverlayCallbacks);
+        postAsyncCallback(mHandler, mDeferredOverlayCallbacks);
+    }
+
+    private void checkIfOverlayStillDeferred() {
+        if (!mDeferOverlayCallbacks) {
+            return;
+        }
+        if (isStarted() && (!hasBeenResumed()
+                || mStateManager.getState().hasFlag(FLAG_NON_INTERACTIVE))) {
+            return;
+        }
+        mDeferOverlayCallbacks = false;
+
+        // Move the client to the correct state. Calling the same method twice is no-op.
+        if (isStarted()) {
+            mOverlayManager.onActivityStarted(this);
+        }
+        if (hasBeenResumed()) {
+            mOverlayManager.onActivityResumed(this);
+        } else {
+            mOverlayManager.onActivityPaused(this);
+        }
+        if (!isStarted()) {
+            mOverlayManager.onActivityStopped(this);
+        }
+    }
+
+    public void deferOverlayCallbacksUntilNextResumeOrStop() {
+        mDeferOverlayCallbacks = true;
+    }
+
+    public LauncherOverlayManager getOverlayManager() {
+        return mOverlayManager;
+    }
+
+    @Override
     public void onStateSetStart(LauncherState state) {
-        if (mDeferredResumePending) {
-            handleDeferredResume();
+        super.onStateSetStart(state);
+        if (mDeferOverlayCallbacks) {
+            scheduleDeferredCheck();
         }
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onStateChanged();
+        addActivityFlags(ACTIVITY_STATE_TRANSITION_ACTIVE);
+
+        if (state.hasFlag(FLAG_CLOSE_POPUPS)) {
+            AbstractFloatingView.closeAllOpenViews(this, !state.hasFlag(FLAG_NON_INTERACTIVE));
         }
+
+        if (state == SPRING_LOADED) {
+            // Prevent any Un/InstallShortcutReceivers from updating the db while we are
+            // not on homescreen
+            InstallShortcutReceiver.enableInstallQueue(FLAG_DRAG_AND_DROP);
+            getRotationHelper().setCurrentStateRequest(REQUEST_LOCK);
+
+            mWorkspace.showPageIndicatorAtCurrentScroll();
+            mWorkspace.setClipChildren(false);
+        }
+        // When multiple pages are visible, show persistent page indicator
+        mWorkspace.getPageIndicator().setShouldAutoHide(!state.hasFlag(FLAG_MULTI_PAGE));
     }
 
+    @Override
     public void onStateSetEnd(LauncherState state) {
+        super.onStateSetStart(state);
         getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
-        getWorkspace().setClipChildren(!state.disablePageClipping);
+        getWorkspace().setClipChildren(!state.hasFlag(FLAG_MULTI_PAGE));
+
         finishAutoCancelActionMode();
+        removeActivityFlags(ACTIVITY_STATE_TRANSITION_ACTIVE);
+
+        // dispatch window state changed
+        getWindow().getDecorView().sendAccessibilityEvent(TYPE_WINDOW_STATE_CHANGED);
+        AccessibilityManagerCompat.sendStateEventToTest(this, state.ordinal);
+
+        if (state == NORMAL) {
+            // Re-enable any Un/InstallShortcutReceiver and now process any queued items
+            InstallShortcutReceiver.disableAndFlushInstallQueue(FLAG_DRAG_AND_DROP, this);
+
+            // Clear any rotation locks when going to normal state
+            getRotationHelper().setCurrentStateRequest(REQUEST_NONE);
+        }
     }
 
     @Override
     protected void onResume() {
-        RaceConditionTracker.onEvent(ON_RESUME_EVT, ENTER);
-        TraceHelper.beginSection("ON_RESUME");
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         super.onResume();
-        TraceHelper.partitionSection("ON_RESUME", "superCall");
-
-        mHandler.removeCallbacks(mHandleDeferredResume);
-        Utilities.postAsyncCallback(mHandler, mHandleDeferredResume);
 
         if (!mOnResumeCallbacks.isEmpty()) {
             final ArrayList<OnResumeCallback> resumeCallbacks = new ArrayList<>(mOnResumeCallbacks);
@@ -988,12 +1052,13 @@
             resumeCallbacks.clear();
         }
 
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onResume();
+        if (mDeferOverlayCallbacks) {
+            scheduleDeferredCheck();
+        } else {
+            mOverlayManager.onActivityResumed(this);
         }
 
-        TraceHelper.endSection("ON_RESUME");
-        RaceConditionTracker.onEvent(ON_RESUME_EVT, EXIT);
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     @Override
@@ -1003,54 +1068,14 @@
 
         super.onPause();
         mDragController.cancelDrag();
-        mDragController.resetLastGestureUpTime();
+        mLastTouchUpTime = -1;
         mDropTargetBar.animateToVisibility(false);
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onPause();
+
+        if (!mDeferOverlayCallbacks) {
+            mOverlayManager.onActivityPaused(this);
         }
     }
 
-    @Override
-    protected void onUserLeaveHint() {
-        super.onUserLeaveHint();
-        UiFactory.onLauncherStateOrResumeChanged(this);
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        super.onWindowFocusChanged(hasFocus);
-        mStateManager.onWindowFocusChanged();
-    }
-
-    public interface LauncherOverlay {
-
-        /**
-         * Touch interaction leading to overscroll has begun
-         */
-        void onScrollInteractionBegin();
-
-        /**
-         * Touch interaction related to overscroll has ended
-         */
-        void onScrollInteractionEnd();
-
-        /**
-         * Scroll progress, between 0 and 100, when the user scrolls beyond the leftmost
-         * screen (or in the case of RTL, the rightmost screen).
-         */
-        void onScrollChange(float progress, boolean rtl);
-
-        /**
-         * Called when the launcher is ready to use the overlay
-         * @param callbacks A set of callbacks provided by Launcher in relation to the overlay
-         */
-        void setOverlayCallbacks(LauncherOverlayCallbacks callbacks);
-    }
-
-    public interface LauncherOverlayCallbacks {
-        void onScrollChanged(float progress);
-    }
-
     class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
 
         public void onScrollChanged(float progress) {
@@ -1060,10 +1085,6 @@
         }
     }
 
-    public boolean isInState(LauncherState state) {
-        return mStateManager.getState() == state;
-    }
-
     /**
      * Restores the previous state, if it exists.
      *
@@ -1077,7 +1098,7 @@
         int stateOrdinal = savedState.getInt(RUNTIME_STATE, NORMAL.ordinal);
         LauncherState[] stateValues = LauncherState.values();
         LauncherState state = stateValues[stateOrdinal];
-        if (!state.disableRestore) {
+        if (!state.shouldDisableRestore()) {
             mStateManager.goToState(state, false /* animated */);
         }
 
@@ -1100,21 +1121,17 @@
     /**
      * Finds all the views we need and configure them properly.
      */
-    private void setupViews() {
+    protected void setupViews() {
         mDragLayer = findViewById(R.id.drag_layer);
         mFocusHandler = mDragLayer.getFocusIndicatorHelper();
         mWorkspace = mDragLayer.findViewById(R.id.workspace);
         mWorkspace.initParentViews(mDragLayer);
         mOverviewPanel = findViewById(R.id.overview_panel);
         mHotseat = findViewById(R.id.hotseat);
-
-        mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        mHotseat.setWorkspace(mWorkspace);
 
         // Setup the drag layer
         mDragLayer.setup(mDragController, mWorkspace);
-        mCancelTouchController = UiFactory.enableLiveUIChanges(this);
 
         mWorkspace.setup(mDragController);
         // Until the workspace is bound, ensure that we keep the wallpaper offset locked to the
@@ -1133,10 +1150,9 @@
         mScrimView = findViewById(R.id.scrim_view);
 
         // Setup the drag controller (drop targets have to be added in reverse order in priority)
-        mDragController.setMoveTarget(mWorkspace);
         mDropTargetBar.setup(mDragController);
 
-        mAllAppsController.setupViews(mAppsView);
+        mAllAppsController.setupViews(mAppsView, mScrimView);
     }
 
     /**
@@ -1181,8 +1197,8 @@
 
         WorkspaceItemInfo info = null;
         if (Utilities.ATLEAST_OREO) {
-            info = LauncherAppsCompatVO.createWorkspaceItemFromPinItemRequest(
-                    this, LauncherAppsCompatVO.getPinItemRequest(data), 0);
+            info = PinRequestHelper.createWorkspaceItemFromPinItemRequest(
+                    this, PinRequestHelper.getPinItemRequest(data), 0);
         }
 
         if (info == null) {
@@ -1211,13 +1227,13 @@
                 cellXY[1] = cellY;
                 foundCellSpan = true;
 
+                DragObject dragObject = new DragObject(getApplicationContext());
+                dragObject.dragInfo = info;
                 // If appropriate, either create a folder or add to an existing folder
                 if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, 0,
-                        true, null)) {
+                        true, dragObject)) {
                     return;
                 }
-                DragObject dragObject = new DragObject();
-                dragObject.dragInfo = info;
                 if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, 0, dragObject,
                         true)) {
                     return;
@@ -1280,6 +1296,7 @@
         hostView.setVisibility(View.VISIBLE);
         prepareAppWidget(hostView, launcherInfo);
         mWorkspace.addInScreen(hostView, launcherInfo);
+        announceForAccessibility(R.string.item_added_to_workspace);
     }
 
     private void prepareAppWidget(AppWidgetHostView hostView, LauncherAppWidgetInfo item) {
@@ -1295,12 +1312,15 @@
             // Reset AllApps to its initial state only if we are not in the middle of
             // processing a multi-step drop
             if (mPendingRequestArgs == null) {
+                if (!isInState(NORMAL)) {
+                    onUiChangedWhileSleeping();
+                }
                 mStateManager.goToState(NORMAL);
             }
         }
     };
 
-    public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
+    private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
         mWorkspace.updateNotificationDots(updatedDots);
         mAppsView.getAppsStore().updateNotificationDots(updatedDots);
     }
@@ -1308,19 +1328,14 @@
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onAttachedToWindow();
-        }
+        mOverlayManager.onAttachedToWindow();
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onDetachedFromWindow();
-        }
+        mOverlayManager.onDetachedFromWindow();
+        closeContextMenu();
     }
 
     public AllAppsTransitionController getAllAppsController() {
@@ -1328,11 +1343,6 @@
     }
 
     @Override
-    public LauncherRootView getRootView() {
-        return (LauncherRootView) mLauncherView;
-    }
-
-    @Override
     public DragLayer getDragLayer() {
         return mDragLayer;
     }
@@ -1357,6 +1367,10 @@
         return mDropTargetBar;
     }
 
+    public ScrimView getScrimView() {
+        return mScrimView;
+    }
+
     public LauncherAppWidgetHost getAppWidgetHost() {
         return mAppWidgetHost;
     }
@@ -1369,17 +1383,26 @@
         return mModelWriter;
     }
 
+    @Override
     public SharedPreferences getSharedPrefs() {
         return mSharedPrefs;
     }
 
+    @Override
+    public SharedPreferences getDevicePrefs() {
+        return Utilities.getDevicePrefs(this);
+    }
+
     public int getOrientation() {
         return mOldConfig.orientation;
     }
 
     @Override
     protected void onNewIntent(Intent intent) {
-        TraceHelper.beginSection("NEW_INTENT");
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
+        }
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
         super.onNewIntent(intent);
 
         boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags() &
@@ -1390,14 +1413,12 @@
         boolean shouldMoveToDefaultScreen = alreadyOnHome && isInState(NORMAL)
                 && AbstractFloatingView.getTopOpenView(this) == null;
         boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
-        boolean internalStateHandled = InternalStateHandler
-                .handleNewIntent(this, intent, isStarted());
+        boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this, intent);
 
         if (isActionMain) {
             if (!internalStateHandled) {
                 // In all these cases, only animate if we're already on home
-                AbstractFloatingView.closeAllOpenViews(this, isStarted());
-                UiFactory.closeSystemWindows();
+                closeOpenViews(isStarted());
 
                 if (!isInState(NORMAL)) {
                     // Only change state, if not already the same. This prevents cancelling any
@@ -1421,18 +1442,27 @@
             target.pageIndex = mWorkspace.getCurrentPage();
             ued.logActionCommand(Action.Command.HOME_INTENT, target,
                     newContainerTarget(ContainerType.WORKSPACE));
-
-            final View v = getWindow().peekDecorView();
-            if (v != null && v.getWindowToken() != null) {
-                UiThreadHelper.hideKeyboardAsync(this, v.getWindowToken());
-            }
+            hideKeyboard();
 
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
+            mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+        } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
+            getStateManager().goToState(ALL_APPS, alreadyOnHome);
         }
 
-        TraceHelper.endSection("NEW_INTENT");
+        TraceHelper.INSTANCE.endSection(traceToken);
+    }
+
+    /**
+     * Hides the keyboard if visible
+     */
+    public void hideKeyboard() {
+        final View v = getWindow().peekDecorView();
+        if (v != null && v.getWindowToken() != null) {
+            UiThreadHelper.hideKeyboardAsync(this, v.getWindowToken());
+        }
     }
 
     @Override
@@ -1475,31 +1505,19 @@
         }
 
         super.onSaveInstanceState(outState);
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onSaveInstanceState(outState);
-        }
+        mOverlayManager.onActivitySaveInstanceState(this, outState);
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
+        ACTIVITY_TRACKER.onActivityDestroyed(this);
 
         unregisterReceiver(mScreenOffReceiver);
         mWorkspace.removeFolderListeners();
+        PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
 
-        if (mCancelTouchController != null) {
-            mCancelTouchController.run();
-            mCancelTouchController = null;
-        }
-
-        // Stop callbacks from LauncherModel
-        // It's possible to receive onDestroy after a new Launcher activity has
-        // been created. In this case, don't interfere with the new Launcher.
-        if (mModel.isCurrentCallbacks(this)) {
-            mModel.stopLoader();
-            LauncherAppState.getInstance(this).setLauncher(null);
-        }
+        mModel.removeCallbacks(this);
         mRotationHelper.destroy();
 
         try {
@@ -1511,9 +1529,11 @@
         TextKeyListener.getInstance().release();
         clearPendingBinds();
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onDestroy();
-        }
+
+        mOverlayManager.onActivityDestroyed(this);
+        mAppTransitionManager.unregisterRemoteAnimations();
+        mUserChangedCallbackCloseable.close();
+        mAllAppsController.onActivityDestroyed();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1529,10 +1549,7 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        if (requestCode == -1
-                || !UiFactory.startActivityForResult(this, intent, requestCode, options)) {
-            super.startActivityForResult(intent, requestCode, options);
-        }
+        super.startActivityForResult(intent, requestCode, options);
     }
 
     @Override
@@ -1541,14 +1558,11 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        if (requestCode == -1 || !UiFactory.startIntentSenderForResult(this, intent, requestCode,
-                fillInIntent, flagsMask, flagsValues, extraFlags, options)) {
-            try {
-                super.startIntentSenderForResult(intent, requestCode,
-                        fillInIntent, flagsMask, flagsValues, extraFlags, options);
-            } catch (IntentSender.SendIntentException e) {
-                throw new ActivityNotFoundException();
-            }
+        try {
+            super.startIntentSenderForResult(intent, requestCode,
+                    fillInIntent, flagsMask, flagsValues, extraFlags, options);
+        } catch (IntentSender.SendIntentException e) {
+            throw new ActivityNotFoundException();
         }
     }
 
@@ -1613,7 +1627,7 @@
             };
             completeAddAppWidget(appWidgetId, info, boundWidget,
                     addFlowHandler.getProviderInfo(this));
-            mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false);
+            mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
         }
     }
 
@@ -1647,6 +1661,7 @@
     private void processShortcutFromDrop(PendingAddShortcutInfo info) {
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setComponent(info.componentName);
         setWaitingForResult(PendingRequestArgs.forIntent(REQUEST_CREATE_SHORTCUT, intent, info));
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: processShortcutFromDrop");
         if (!info.activityInfo.startConfigActivity(this, REQUEST_CREATE_SHORTCUT)) {
             handleActivityResult(REQUEST_CREATE_SHORTCUT, RESULT_CANCELED, null);
         }
@@ -1692,16 +1707,19 @@
         }
     }
 
-    FolderIcon addFolder(CellLayout layout, int container, final int screenId, int cellX,
+    /**
+     * Creates and adds new folder to CellLayout
+     */
+    public FolderIcon addFolder(CellLayout layout, int container, final int screenId, int cellX,
             int cellY) {
         final FolderInfo folderInfo = new FolderInfo();
-        folderInfo.title = "";
 
         // Update the model
         getModelWriter().addItemToDatabase(folderInfo, container, screenId, cellX, cellY);
 
         // Create the view
-        FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo);
+        FolderIcon newFolder = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this, layout,
+                folderInfo);
         mWorkspace.addInScreen(newFolder, folderInfo);
         // Force measure the new folder icon
         CellLayout parent = mWorkspace.getParentCellLayoutForView(newFolder);
@@ -1710,6 +1728,16 @@
     }
 
     /**
+     * Called when a workspace item is converted into a folder
+     */
+    public void folderCreatedFromItem(Folder folder, WorkspaceItemInfo itemInfo){}
+
+    /**
+     * Called when a folder is converted into a workspace item
+     */
+    public void folderConvertedToItem(Folder folder, WorkspaceItemInfo itemInfo) {}
+
+    /**
      * Unbinds the view for the specified item, and removes the item and all its children.
      *
      * @param v the view being removed.
@@ -1751,17 +1779,39 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Key event", event);
         return (event.getKeyCode() == KeyEvent.KEYCODE_HOME) || super.dispatchKeyEvent(event);
     }
 
     @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mTouchInProgress = true;
+                break;
+            case MotionEvent.ACTION_UP:
+                mLastTouchUpTime = System.currentTimeMillis();
+                // Follow through
+            case MotionEvent.ACTION_CANCEL:
+                mTouchInProgress = false;
+                break;
+        }
+        TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
+        return super.dispatchTouchEvent(ev);
+    }
+
+    /**
+     * Returns true if a touch interaction is in progress
+     */
+    public boolean isTouchInProgress() {
+        return mTouchInProgress;
+    }
+
+    @Override
     public void onBackPressed() {
         if (finishAutoCancelActionMode()) {
             return;
         }
-        if (mLauncherCallbacks != null && mLauncherCallbacks.handleBackPressed()) {
-            return;
-        }
 
         if (mDragController.isDragging()) {
             mDragController.cancelDrag();
@@ -1770,7 +1820,6 @@
 
         // Note: There should be at most one log per method call. This is enforced implicitly
         // by using if-else statements.
-        UserEventDispatcher ued = getUserEventDispatcher();
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this);
         if (topView != null && topView.onBackPressed()) {
             // Handled by the floating view.
@@ -1811,33 +1860,6 @@
     }
 
     @Override
-    public int getCurrentState() {
-        if (mStateManager.getState() == LauncherState.ALL_APPS) {
-            return StatsLogUtils.LAUNCHER_STATE_ALLAPPS;
-        } else if (mStateManager.getState() == OVERVIEW) {
-            return StatsLogUtils.LAUNCHER_STATE_OVERVIEW;
-        }
-        return StatsLogUtils.LAUNCHER_STATE_HOME;
-    }
-
-    @Override
-    public void modifyUserEvent(LauncherLogProto.LauncherEvent event) {
-        if (event.srcTarget != null && event.srcTarget.length > 0 &&
-                event.srcTarget[1].containerType == ContainerType.PREDICTION) {
-            Target[] targets = new Target[3];
-            targets[0] = event.srcTarget[0];
-            targets[1] = event.srcTarget[1];
-            targets[2] = newTarget(Target.Type.CONTAINER);
-            event.srcTarget = targets;
-            LauncherState state = mStateManager.getState();
-            if (state == LauncherState.ALL_APPS) {
-                event.srcTarget[2].containerType = ContainerType.ALLAPPS;
-            } else if (state == OVERVIEW) {
-                event.srcTarget[2].containerType = ContainerType.TASKSWITCHER;
-            }
-        }
-    }
-
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
             @Nullable String sourceContainer) {
         if (!hasBeenResumed()) {
@@ -1845,7 +1867,10 @@
             // recents animation into launcher. Defer launching the activity until Launcher is
             // next resumed.
             addOnResumeCallback(() -> startActivitySafely(v, intent, item, sourceContainer));
-            UiFactory.clearSwipeSharedState(true /* finishAnimation */);
+            if (mOnDeferredActivityLaunchCallback != null) {
+                mOnDeferredActivityLaunchCallback.run();
+                mOnDeferredActivityLaunchCallback = null;
+            }
             return true;
         }
 
@@ -1886,10 +1911,6 @@
             // This clears all widget bitmaps from the widget tray
             // TODO(hyunyoungs)
         }
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onTrimMemory(level);
-        }
-        UiFactory.onTrimMemory(this, level);
     }
 
     @Override
@@ -1910,11 +1931,29 @@
     }
 
     /**
+     * Persistant callback which notifies when an activity launch is deferred because the activity
+     * was not yet resumed.
+     */
+    public void setOnDeferredActivityLaunchCallback(Runnable callback) {
+        mOnDeferredActivityLaunchCallback = callback;
+    }
+
+    /**
+     * Sets the next page to bind synchronously on next bind.
+     * @param page
+     */
+    public void setPageToBindSynchronously(int page) {
+        mPageToBindSynchronously = page;
+    }
+
+    /**
      * Implementation of the method from LauncherModel.Callbacks.
      */
     @Override
-    public int getCurrentWorkspaceScreen() {
-        if (mWorkspace != null) {
+    public int getPageToBindSynchronously() {
+        if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
+            return mPageToBindSynchronously;
+        } else  if (mWorkspace != null) {
             return mWorkspace.getCurrentPage();
         } else {
             return 0;
@@ -1943,7 +1982,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void startBinding() {
-        TraceHelper.beginSection("startBinding");
+        Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding");
         // Floating panels (except the full widget sheet) are associated with individual icons. If
         // we are starting a fresh bind, close all such panels as all the icons are about
         // to go away.
@@ -1959,9 +1998,9 @@
         mAppWidgetHost.clearViews();
 
         if (mHotseat != null) {
-            mHotseat.resetLayout(getWallpaperDeviceProfile().isVerticalBarLayout());
+            mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
         }
-        TraceHelper.endSection("startBinding");
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     @Override
@@ -2023,7 +2062,7 @@
         }
 
         // Remove the extra empty screen
-        mWorkspace.removeExtraEmptyScreen(false, false);
+        mWorkspace.removeExtraEmptyScreen(false);
     }
 
     /**
@@ -2058,7 +2097,7 @@
                     break;
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
-                    view = FolderIcon.fromXml(R.layout.folder_icon, this,
+                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
                             (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
                             (FolderInfo) item);
                     break;
@@ -2085,7 +2124,7 @@
                     Object tag = v.getTag();
                     String desc = "Collision while binding workspace item: " + item
                             + ". Collides with " + tag;
-                    if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    if (FeatureFlags.IS_STUDIO_BUILD) {
                         throw (new RuntimeException(desc));
                     } else {
                         Log.d(TAG, desc);
@@ -2120,7 +2159,7 @@
                 mWorkspace.postDelayed(new Runnable() {
                     public void run() {
                         if (mWorkspace != null) {
-                            AbstractFloatingView.closeAllOpenViews(Launcher.this, false);
+                            closeOpenViews(false);
 
                             mWorkspace.snapToPage(newScreenIndex);
                             mWorkspace.postDelayed(startBounceAnimRunnable,
@@ -2135,6 +2174,9 @@
         workspace.requestLayout();
     }
 
+    @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
     /**
      * Add the views for a widget to the workspace.
      */
@@ -2154,112 +2196,125 @@
                 return null;
             }
         }
-
+        final AppWidgetHostView view;
         if (mIsSafeModeEnabled) {
-            PendingAppWidgetHostView view =
-                    new PendingAppWidgetHostView(this, item, mIconCache, true);
+            view = new PendingAppWidgetHostView(this, item, mIconCache, true);
             prepareAppWidget(view, item);
             return view;
         }
 
-        TraceHelper.beginSection("BIND_WIDGET");
+        Object traceToken = TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
 
-        final LauncherAppWidgetProviderInfo appWidgetInfo;
+        try {
+            final LauncherAppWidgetProviderInfo appWidgetInfo;
 
-        if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
-            // If the provider is not ready, bind as a pending widget.
-            appWidgetInfo = null;
-        } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-            // The widget id is not valid. Try to find the widget based on the provider info.
-            appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user);
-        } else {
-            appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId);
-        }
-
-        // If the provider is ready, but the width is not yet restored, try to restore it.
-        if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) &&
-                (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
-            if (appWidgetInfo == null) {
-                Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId
-                        + " belongs to component " + item.providerName
-                        + ", as the provider is null");
-                getModelWriter().deleteItemFromDatabase(item);
-                return null;
+            if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
+                // If the provider is not ready, bind as a pending widget.
+                appWidgetInfo = null;
+            } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+                // The widget id is not valid. Try to find the widget based on the provider info.
+                appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user);
+            } else {
+                appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId);
             }
 
-            // If we do not have a valid id, try to bind an id.
-            if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-                if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
-                    // Id has not been allocated yet. Allocate a new id.
-                    item.appWidgetId = mAppWidgetHost.allocateAppWidgetId();
-                    item.restoreStatus |= LauncherAppWidgetInfo.FLAG_ID_ALLOCATED;
+            // If the provider is ready, but the width is not yet restored, try to restore it.
+            if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                    && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
+                if (appWidgetInfo == null) {
+                    Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId
+                            + " belongs to component " + item.providerName
+                            + ", as the provider is null");
+                    getModelWriter().deleteItemFromDatabase(item);
+                    return null;
+                }
 
-                    // Also try to bind the widget. If the bind fails, the user will be shown
-                    // a click to setup UI, which will ask for the bind permission.
-                    PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo);
-                    pendingInfo.spanX = item.spanX;
-                    pendingInfo.spanY = item.spanY;
-                    pendingInfo.minSpanX = item.minSpanX;
-                    pendingInfo.minSpanY = item.minSpanY;
-                    Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this,
-                            pendingInfo);
+                // If we do not have a valid id, try to bind an id.
+                if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+                    if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
+                        // Id has not been allocated yet. Allocate a new id.
+                        item.appWidgetId = mAppWidgetHost.allocateAppWidgetId();
+                        item.restoreStatus |= LauncherAppWidgetInfo.FLAG_ID_ALLOCATED;
 
-                    boolean isDirectConfig =
-                            item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
-                    if (isDirectConfig && item.bindOptions != null) {
-                        Bundle newOptions = item.bindOptions.getExtras();
-                        if (options != null) {
-                            newOptions.putAll(options);
+                        // Also try to bind the widget. If the bind fails, the user will be shown
+                        // a click to setup UI, which will ask for the bind permission.
+                        PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo);
+                        pendingInfo.spanX = item.spanX;
+                        pendingInfo.spanY = item.spanY;
+                        pendingInfo.minSpanX = item.minSpanX;
+                        pendingInfo.minSpanY = item.minSpanY;
+                        Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this,
+                                pendingInfo);
+
+                        boolean isDirectConfig =
+                                item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
+                        if (isDirectConfig && item.bindOptions != null) {
+                            Bundle newOptions = item.bindOptions.getExtras();
+                            if (options != null) {
+                                newOptions.putAll(options);
+                            }
+                            options = newOptions;
                         }
-                        options = newOptions;
+                        boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
+                                item.appWidgetId, appWidgetInfo, options);
+
+                        // We tried to bind once. If we were not able to bind, we would need to
+                        // go through the permission dialog, which means we cannot skip the config
+                        // activity.
+                        item.bindOptions = null;
+                        item.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG;
+
+                        // Bind succeeded
+                        if (success) {
+                            // If the widget has a configure activity, it is still needs to set it
+                            // up, otherwise the widget is ready to go.
+                            item.restoreStatus = (appWidgetInfo.configure == null) || isDirectConfig
+                                    ? LauncherAppWidgetInfo.RESTORE_COMPLETED
+                                    : LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+                        }
+
+                        getModelWriter().updateItemInDatabase(item);
                     }
-                    boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
-                            item.appWidgetId, appWidgetInfo, options);
-
-                    // We tried to bind once. If we were not able to bind, we would need to
-                    // go through the permission dialog, which means we cannot skip the config
-                    // activity.
-                    item.bindOptions = null;
-                    item.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG;
-
-                    // Bind succeeded
-                    if (success) {
-                        // If the widget has a configure activity, it is still needs to set it up,
-                        // otherwise the widget is ready to go.
-                        item.restoreStatus = (appWidgetInfo.configure == null) || isDirectConfig
-                                ? LauncherAppWidgetInfo.RESTORE_COMPLETED
-                                : LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-                    }
-
+                } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
+                        && (appWidgetInfo.configure == null)) {
+                    // The widget was marked as UI not ready, but there is no configure activity to
+                    // update the UI.
+                    item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
                     getModelWriter().updateItemInDatabase(item);
                 }
-            } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
-                    && (appWidgetInfo.configure == null)) {
-                // The widget was marked as UI not ready, but there is no configure activity to
-                // update the UI.
-                item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
-                getModelWriter().updateItemInDatabase(item);
-            }
-        }
-
-        final AppWidgetHostView view;
-        if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
-            // Verify that we own the widget
-            if (appWidgetInfo == null) {
-                FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
-                getModelWriter().deleteWidgetInfo(item, getAppWidgetHost());
-                return null;
+                else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
+                        && appWidgetInfo.configure != null) {
+                    if (mAppWidgetManager.isAppWidgetRestored(item.appWidgetId)) {
+                        item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
+                        getModelWriter().updateItemInDatabase(item);
+                    }
+                }
             }
 
-            item.minSpanX = appWidgetInfo.minSpanX;
-            item.minSpanY = appWidgetInfo.minSpanY;
-            view = mAppWidgetHost.createView(this, item.appWidgetId, appWidgetInfo);
-        } else {
-            view = new PendingAppWidgetHostView(this, item, mIconCache, false);
-        }
-        prepareAppWidget(view, item);
+            if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
+                // Verify that we own the widget
+                if (appWidgetInfo == null) {
+                    FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
+                    getModelWriter().deleteWidgetInfo(item, getAppWidgetHost());
+                    return null;
+                }
 
-        TraceHelper.endSection("BIND_WIDGET", "id=" + item.appWidgetId);
+                item.minSpanX = appWidgetInfo.minSpanX;
+                item.minSpanY = appWidgetInfo.minSpanY;
+                view = mAppWidgetHost.createView(this, item.appWidgetId, appWidgetInfo);
+            } else if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)
+                    && appWidgetInfo != null) {
+                mAppWidgetHost.addPendingView(item.appWidgetId,
+                        new PendingAppWidgetHostView(this, item, mIconCache, false));
+                view = mAppWidgetHost.createView(this, item.appWidgetId, appWidgetInfo);
+            } else {
+                view = new PendingAppWidgetHostView(this, item, mIconCache, false);
+            }
+            prepareAppWidget(view, item);
+        } finally {
+            TraceHelper.INSTANCE.endSection(traceToken);
+        }
+
         return view;
     }
 
@@ -2291,6 +2346,8 @@
 
     public void onPageBoundSynchronously(int page) {
         mSynchronouslyBoundPage = page;
+        mWorkspace.setCurrentPage(page);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
     }
 
     @Override
@@ -2337,7 +2394,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void finishBindingItems(int pageBoundFirst) {
-        TraceHelper.beginSection("finishBindingItems");
+        Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
         mWorkspace.restoreInstanceStateForRemainingPages();
 
         setWorkspaceLoading(false);
@@ -2355,18 +2412,23 @@
         // Since we are just resetting the current page without user interaction,
         // override the previous page so we don't log the page switch.
         mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
         // Cache one page worth of icons
         getViewCache().setCacheSize(R.layout.folder_application,
                 mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
         getViewCache().setCacheSize(R.layout.folder_page, 2);
 
-        TraceHelper.endSection("finishBindingItems");
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     private boolean canRunNewAppsAnimation() {
-        long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime();
-        return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
+        if (mDragController.isDragging()) {
+            return false;
+        } else {
+            return (System.currentTimeMillis() - mLastTouchUpTime)
+                    > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
+        }
     }
 
     private ValueAnimator createNewAppBounceAnimation(View v, int i) {
@@ -2377,13 +2439,18 @@
         return bounceAnim;
     }
 
+    private void announceForAccessibility(@StringRes int stringResId) {
+        getDragLayer().announceForAccessibility(getString(stringResId));
+    }
+
     /**
      * Add the icons for all apps.
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    public void bindAllApplications(AppInfo[] apps) {
-        mAppsView.getAppsStore().setApps(apps);
+    @Override
+    public void bindAllApplications(AppInfo[] apps, int flags) {
+        mAppsView.getAppsStore().setApps(apps, flags);
     }
 
     /**
@@ -2493,9 +2560,10 @@
         writer.println(prefix + "\tmRotationHelper: " + mRotationHelper);
         writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening());
 
-        // Extra logging for b/116853349
+        // Extra logging for general debugging
         mDragLayer.dump(prefix, writer);
         mStateManager.dump(prefix, writer);
+        mPopupDataProvider.dump(prefix, writer);
 
         try {
             FileLog.flushAll(writer);
@@ -2508,6 +2576,7 @@
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.dump(prefix, fd, writer, args);
         }
+        mOverlayManager.dump(prefix, writer);
     }
 
     @Override
@@ -2587,10 +2656,13 @@
             if (!mDragController.isDragging() && !mWorkspace.isSwitchingState() &&
                     isInState(NORMAL)) {
                 // Close any open floating views.
-                AbstractFloatingView.closeAllOpenViews(this);
+                closeOpenViews();
 
                 // Setting the touch point to (-1, -1) will show the options popup in the center of
                 // the screen.
+                if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on key up");
+                }
                 OptionsPopupView.showDefaultOptions(this, -1, -1);
             }
             return true;
@@ -2598,16 +2670,48 @@
         return super.onKeyUp(keyCode, event);
     }
 
-    public static Launcher getLauncher(Context context) {
-        return (Launcher) fromContext(context);
+    protected StateHandler<LauncherState>[] createStateHandlers() {
+        return new StateHandler[] { getAllAppsController(), getWorkspace() };
     }
 
+    public TouchController[] createTouchControllers() {
+        return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
+    }
+
+    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
+
+    public void onDragLayerHierarchyChanged() { }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
         getStateManager().goToState(LauncherState.NORMAL);
     }
 
+    private void closeOpenViews() {
+        closeOpenViews(true);
+    }
+
+    protected void closeOpenViews(boolean animate) {
+        AbstractFloatingView.closeAllOpenViews(this, animate);
+    }
+
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.of(APP_INFO, WIDGETS, INSTALL);
+    }
+
+
+    /**
+     * @see LauncherState#getOverviewScaleAndOffset(Launcher)
+     */
+    public float[] getNormalOverviewScaleAndOffset() {
+        return new float[] {NO_SCALE, NO_OFFSET};
+    }
+
+    public static Launcher getLauncher(Context context) {
+        return fromContext(context);
+    }
+
     /**
      * Just a wrapper around the type cast to allow easier tracking of calls.
      */
@@ -2615,6 +2719,7 @@
         return (T) activityContext;
     }
 
+
     /**
      * Callback for listening for onResume
      */
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index 74362ed..d9cf7f1 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -18,7 +18,7 @@
 
 import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
-import android.util.Property;
+import android.util.IntProperty;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 
@@ -27,23 +27,20 @@
      * Durations for various state animations. These are not defined in resources to allow
      * easier access from static classes and enums
      */
-    public static final int ALL_APPS_TRANSITION_MS = 320;
-    public static final int OVERVIEW_TRANSITION_MS = 250;
-    public static final int SPRING_LOADED_TRANSITION_MS = 150;
     public static final int SPRING_LOADED_EXIT_DELAY = 500;
 
     // The progress of an animation to all apps must be at least this far along to snap to all apps.
     public static final float MIN_PROGRESS_TO_ALL_APPS = 0.5f;
 
-    public static final Property<Drawable, Integer> DRAWABLE_ALPHA =
-            new Property<Drawable, Integer>(Integer.TYPE, "drawableAlpha") {
+    public static final IntProperty<Drawable> DRAWABLE_ALPHA =
+            new IntProperty<Drawable>("drawableAlpha") {
                 @Override
                 public Integer get(Drawable drawable) {
                     return drawable.getAlpha();
                 }
 
                 @Override
-                public void set(Drawable drawable, Integer alpha) {
+                public void setValue(Drawable drawable, int alpha) {
                     drawable.setAlpha(alpha);
                 }
             };
@@ -67,28 +64,28 @@
         return (int) Utilities.boundToRange(Math.abs(velocity) / 2, 2f, 6f);
     }
 
-    public static final Property<LayoutParams, Integer> LAYOUT_WIDTH =
-            new Property<LayoutParams, Integer>(Integer.TYPE, "width") {
+    public static final IntProperty<LayoutParams> LAYOUT_WIDTH =
+            new IntProperty<LayoutParams>("width") {
                 @Override
                 public Integer get(LayoutParams lp) {
                     return lp.width;
                 }
 
                 @Override
-                public void set(LayoutParams lp, Integer width) {
+                public void setValue(LayoutParams lp, int width) {
                     lp.width = width;
                 }
             };
 
-    public static final Property<LayoutParams, Integer> LAYOUT_HEIGHT =
-            new Property<LayoutParams, Integer>(Integer.TYPE, "height") {
+    public static final IntProperty<LayoutParams> LAYOUT_HEIGHT =
+            new IntProperty<LayoutParams>("height") {
                 @Override
                 public Integer get(LayoutParams lp) {
                     return lp.height;
                 }
 
                 @Override
-                public void set(LayoutParams lp, Integer height) {
+                public void setValue(LayoutParams lp, int height) {
                     lp.height = height;
                 }
             };
@@ -120,4 +117,18 @@
                             return view.getTranslationY();
                         }
                     };
+
+    public static final FloatProperty<View> VIEW_ALPHA =
+            View.ALPHA instanceof FloatProperty ? (FloatProperty) View.ALPHA
+                    : new FloatProperty<View>("alpha") {
+                        @Override
+                        public void setValue(View view, float v) {
+                            view.setAlpha(v);
+                        }
+
+                        @Override
+                        public Float get(View view) {
+                            return view.getAlpha();
+                        }
+                    };
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index d70abc2..53e5274 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,26 +17,32 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.content.ComponentName;
-import android.content.ContentProviderClient;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
+import android.content.pm.LauncherApps;
 import android.os.Handler;
 import android.util.Log;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.compat.UserManagerCompat;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.PredictionModel;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.pm.InstallSessionTracker;
+import com.android.launcher3.pm.UserCache;
 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;
 
 public class LauncherAppState {
@@ -44,7 +50,7 @@
     public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
-    private static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
+    public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
             new MainThreadInitializedObject<>(LauncherAppState::new);
 
     private final Context mContext;
@@ -52,7 +58,13 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
-    private final SecureSettingsObserver mNotificationDotsObserver;
+    private final PredictionModel mPredictionModel;
+
+    private SecureSettingsObserver mNotificationDotsObserver;
+    private InstallSessionTracker mInstallSessionTracker;
+    private SimpleBroadcastReceiver mModelChangeReceiver;
+    private SafeCloseable mCalendarChangeTracker;
+    private SafeCloseable mUserChangeListener;
 
     public static LauncherAppState getInstance(final Context context) {
         return INSTANCE.get(context);
@@ -66,42 +78,37 @@
         return mContext;
     }
 
-    private LauncherAppState(Context context) {
-        if (getLocalProvider(context) == null) {
-            throw new RuntimeException(
-                    "Initializing LauncherAppState in the absence of LauncherProvider");
+    public LauncherAppState(Context context) {
+        this(context, LauncherFiles.APP_ICONS_DB);
+
+        mModelChangeReceiver = new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
+
+        mContext.getSystemService(LauncherApps.class).registerCallback(mModel);
+        mModelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
+                Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
+                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
+                Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
+        if (FeatureFlags.IS_STUDIO_BUILD) {
+            mModelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
         }
-        Log.v(Launcher.TAG, "LauncherAppState initiated");
-        Preconditions.assertUIThread();
-        mContext = context;
 
-        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(mContext);
-        mIconCache = new IconCache(mContext, mInvariantDeviceProfile);
-        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
-        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+        mCalendarChangeTracker = IconProvider.registerIconChangeListener(mContext,
+                mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
 
-        LauncherAppsCompat.getInstance(mContext).addOnAppsChangedCallback(mModel);
-
-        // Register intent receivers
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
-        // For handling managed profiles
-        filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
-        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
-        filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
-        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
-        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
-
-        if (FeatureFlags.IS_DOGFOOD_BUILD) {
-            filter.addAction(ACTION_FORCE_ROLOAD);
-        }
+        // TODO: remove listener on terminate
         FeatureFlags.APP_SEARCH_IMPROVEMENTS.addChangeListener(context, mModel::forceReload);
+        CustomWidgetManager.INSTANCE.get(mContext)
+                .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
 
-        mContext.registerReceiver(mModel, filter);
-        UserManagerCompat.getInstance(mContext).enableAndResetCache();
+        mUserChangeListener = UserCache.INSTANCE.get(mContext)
+                .addUserChangeListener(mModel::forceReload);
+
         mInvariantDeviceProfile.addOnChangeListener(this::onIdpChanged);
         new Handler().post( () -> mInvariantDeviceProfile.verifyConfigChangedInBackground(context));
 
+        mInstallSessionTracker = InstallSessionHelper.INSTANCE.get(context)
+                .registerInstallTracker(mModel, MODEL_EXECUTOR);
+
         if (!mContext.getResources().getBoolean(R.bool.notification_dots_enabled)) {
             mNotificationDotsObserver = null;
         } else {
@@ -113,6 +120,18 @@
         }
     }
 
+    public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
+        Log.v(Launcher.TAG, "LauncherAppState initiated");
+        Preconditions.assertUIThread();
+        mContext = context;
+
+        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
+        mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
+        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
+        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+        mPredictionModel = PredictionModel.newInstance(mContext);
+    }
+
     protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
         if (areNotificationDotsEnabled) {
             NotificationListener.requestRebind(new ComponentName(
@@ -138,23 +157,26 @@
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
     public void onTerminate() {
-        mContext.unregisterReceiver(mModel);
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext);
-        launcherApps.removeOnAppsChangedCallback(mModel);
-        PackageInstallerCompat.getInstance(mContext).onStop();
+        if (mModelChangeReceiver != null) {
+            mContext.unregisterReceiver(mModelChangeReceiver);
+        }
+        mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
+        if (mInstallSessionTracker != null) {
+            mInstallSessionTracker.unregister();
+        }
+        if (mCalendarChangeTracker != null) {
+            mCalendarChangeTracker.close();
+        }
+        if (mUserChangeListener != null) {
+            mUserChangeListener.close();
+        }
+        CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
+
         if (mNotificationDotsObserver != null) {
             mNotificationDotsObserver.unregister();
         }
     }
 
-    LauncherModel setLauncher(Launcher launcher) {
-        getLocalProvider(mContext).setLauncherProviderChangeListener(launcher);
-        mModel.initialize(launcher);
-        CustomWidgetManager.INSTANCE.get(launcher)
-                .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
-        return mModel;
-    }
-
     public IconCache getIconCache() {
         return mIconCache;
     }
@@ -163,6 +185,10 @@
         return mModel;
     }
 
+    public PredictionModel getPredictionModel() {
+        return mPredictionModel;
+    }
+
     public WidgetPreviewLoader getWidgetCache() {
         return mWidgetCache;
     }
@@ -177,11 +203,4 @@
     public static InvariantDeviceProfile getIDP(Context context) {
         return InvariantDeviceProfile.INSTANCE.get(context);
     }
-
-    private static LauncherProvider getLocalProvider(Context context) {
-        try (ContentProviderClient cl = context.getContentResolver()
-                .acquireContentProviderClient(LauncherProvider.AUTHORITY)) {
-            return (LauncherProvider) cl.getLocalContentProvider();
-        }
-    }
 }
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index c55c120..24e0d14 100644
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ b/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -13,11 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.launcher3;
 
-
-import android.animation.Animator;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.graphics.Rect;
@@ -58,13 +55,16 @@
     }
 
     /**
-     * Number of animations which run on state properties.
+     * Registers remote animations for certain system transitions.
      */
-    public int getStateElementAnimationsCount() {
-        return 0;
+    public void registerRemoteAnimations() {
+        // Do nothing
     }
 
-    public Animator createStateElementAnimation(int index, float... values) {
-        throw new RuntimeException("Unknown gesture animation " + index);
+    /**
+     * Unregisters all remote animations.
+     */
+    public void unregisterRemoteAnimations() {
+        // Do nothing
     }
 }
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index 1215d43..7ea6851 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -29,9 +29,12 @@
 import android.util.SparseArray;
 import android.widget.Toast;
 
-import com.android.launcher3.config.FeatureFlags;
+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;
@@ -53,12 +56,14 @@
 
     private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
     private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
+    private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
 
     private final Context mContext;
     private int mFlags = FLAG_RESUMED;
 
     private IntConsumer mAppWidgetRemovedCallback = null;
 
+
     public LauncherAppWidgetHost(Context context) {
         this(context, null);
     }
@@ -73,14 +78,20 @@
     @Override
     protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
             AppWidgetProviderInfo appWidget) {
-        LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context);
+        final LauncherAppWidgetHostView view;
+        if (mPendingViews.get(appWidgetId) != null) {
+            view = mPendingViews.get(appWidgetId);
+            mPendingViews.remove(appWidgetId);
+        } else {
+            view = new LauncherAppWidgetHostView(context);
+        }
         mViews.put(appWidgetId, view);
         return view;
     }
 
     @Override
     public void startListening() {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             return;
         }
         mFlags |= FLAG_LISTENING;
@@ -107,7 +118,7 @@
 
     @Override
     public void stopListening() {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             return;
         }
         mFlags &= ~FLAG_LISTENING;
@@ -166,7 +177,7 @@
 
     @Override
     public int allocateAppWidgetId() {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             return AppWidgetManager.INVALID_APPWIDGET_ID;
         }
 
@@ -189,6 +200,10 @@
         }
     }
 
+    void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
+        mPendingViews.put(appWidgetId, view);
+    }
+
     public AppWidgetHostView createView(Context context, int appWidgetId,
             LauncherAppWidgetProviderInfo appWidget) {
         if (appWidget.isCustomWidget()) {
@@ -238,8 +253,8 @@
 
     /**
      * Called on an appWidget is removed for a widgetId
-     * @param appWidgetId
-     * TODO: make this override when SDK is updated
+     *
+     * @param appWidgetId TODO: make this override when SDK is updated
      */
     public void onAppWidgetRemoved(int appWidgetId) {
         if (mAppWidgetRemovedCallback == null) {
@@ -263,7 +278,7 @@
     public void startBindFlow(BaseActivity activity,
             int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
 
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             sendActionCancelled(activity, requestCode);
             return;
         }
@@ -279,12 +294,13 @@
 
 
     public void startConfigActivity(BaseActivity activity, int widgetId, int requestCode) {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
             sendActionCancelled(activity, requestCode);
             return;
         }
 
         try {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
             startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, null);
         } catch (ActivityNotFoundException | SecurityException e) {
             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
index 228c07e..618b5de 100644
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
@@ -7,10 +7,13 @@
 import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.UserHandle;
 
-import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 
 /**
  * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords
@@ -19,7 +22,7 @@
  * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo}
  */
 public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo
-        implements ComponentWithLabel {
+        implements ComponentWithLabelAndIcon {
 
     public static final String CLS_CUSTOM_WIDGET_PREFIX = "#custom-widget-";
 
@@ -111,4 +114,9 @@
     public final UserHandle getUser() {
         return getProfile();
     }
+
+    @Override
+    public Drawable getFullResIcon(IconCache cache) {
+        return cache.getFullResIcon(provider.getPackageName(), icon);
+    }
 }
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index dfe75ec..0e529bd 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import android.content.Intent;
 import android.os.Bundle;
 
 import java.io.FileDescriptor;
@@ -36,31 +35,8 @@
      * the code in the corresponding Launcher method is executed.
      */
     void onCreate(Bundle savedInstanceState);
-    void onResume();
-    void onStart();
-    void onStop();
-    void onPause();
-    void onDestroy();
-    void onSaveInstanceState(Bundle outState);
-    void onActivityResult(int requestCode, int resultCode, Intent data);
-    void onRequestPermissionsResult(int requestCode, String[] permissions,
-            int[] grantResults);
-    void onAttachedToWindow();
-    void onDetachedFromWindow();
     void dump(String prefix, FileDescriptor fd, PrintWriter w, String[] args);
     void onHomeIntent(boolean internalStateHandled);
-    boolean handleBackPressed();
-    void onTrimMemory(int level);
-
-    /**
-     * Called when the launcher state changed
-     */
-    default void onStateChanged() { }
-
-    /*
-     * Extension points for providing custom behavior on certain user interactions.
-     */
-    void onLauncherProviderChange();
 
     /**
      * Starts a search with {@param initialQuery}. Return false if search was not started.
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index 9c4646b..25afb55 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -15,8 +15,13 @@
     private static final String XML = ".xml";
 
     public static final String LAUNCHER_DB = "launcher.db";
+    public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
+    public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
+    public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
+    public static final String BACKUP_DB = "backup.db";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
-    public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs";
+    public static final String MANAGED_USER_PREFERENCES_KEY =
+            "com.android.launcher3.managedusers.prefs";
     // This preference file is not backed up to cloud.
     public static final String DEVICE_PREFERENCES_KEY = "com.android.launcher3.device.prefs";
 
@@ -25,6 +30,10 @@
 
     public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
             LAUNCHER_DB,
+            LAUNCHER_4_BY_4_DB,
+            LAUNCHER_3_BY_3_DB,
+            LAUNCHER_2_BY_2_DB,
+            BACKUP_DB,
             SHARED_PREFERENCES_KEY + XML,
             WIDGET_PREVIEWS_DB,
             MANAGED_USER_PREFERENCES_KEY + XML,
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index a012412..f434c91 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -17,27 +17,26 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
+import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
 import android.content.pm.ShortcutInfo;
-import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.AddWorkspaceItemsTask;
 import com.android.launcher3.model.AllAppsList;
@@ -52,16 +51,21 @@
 import com.android.launcher3.model.PackageUpdatedTask;
 import com.android.launcher3.model.ShortcutsChangedTask;
 import com.android.launcher3.model.UserLockStateChangedTask;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.InstallSessionTracker;
+import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -74,17 +78,17 @@
  * LauncherModel object held in a static. Also provide APIs for updating the database state
  * for the Launcher.
  */
-public class LauncherModel extends BroadcastReceiver
-        implements LauncherAppsCompat.OnAppsChangedCallbackCompat {
+public class LauncherModel extends LauncherApps.Callback implements InstallSessionTracker.Callback {
     private static final boolean DEBUG_RECEIVER = false;
 
     static final String TAG = "Launcher.Model";
 
-    @Thunk final LauncherAppState mApp;
-    @Thunk final Object mLock = new Object();
-    @Thunk
-    LoaderTask mLoaderTask;
-    @Thunk boolean mIsLoaderTaskRunning;
+    private final LauncherAppState mApp;
+    private final Object mLock = new Object();
+    private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+    private LoaderTask mLoaderTask;
+    private boolean mIsLoaderTaskRunning;
 
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
@@ -97,7 +101,7 @@
         }
     }
 
-    @Thunk WeakReference<Callbacks> mCallbacks;
+    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
 
     // < only access in worker thread >
     private final AllAppsList mBgAllAppsList;
@@ -106,18 +110,15 @@
      * All the static data should be accessed on the background thread, A lock should be acquired
      * on this object when accessing any data from this model.
      */
-    static final BgDataModel sBgDataModel = new BgDataModel();
+    private final BgDataModel mBgDataModel = new BgDataModel();
 
     // Runnable to check if the shortcuts permission has changed.
     private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
         @Override
         public void run() {
-            if (mModelLoaded) {
-                boolean hasShortcutHostPermission =
-                        DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission();
-                if (hasShortcutHostPermission != sBgDataModel.hasShortcutHostPermission) {
-                    forceReload();
-                }
+            if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
+                    != mBgAllAppsList.hasShortcutHostPermission()) {
+                forceReload();
             }
         }
     };
@@ -127,76 +128,27 @@
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
     }
 
-    public void setPackageState(PackageInstallInfo installInfo) {
-        enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo));
-    }
-
-    /**
-     * Updates the icons and label of all pending icons for the provided package name.
-     */
-    public void updateSessionDisplayInfo(final String packageName, final UserHandle user) {
-        HashSet<String> packages = new HashSet<>();
-        packages.add(packageName);
-        enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_SESSION_UPDATE, user, packages));
-    }
-
     /**
      * Adds the provided items to the workspace.
      */
     public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            callbacks.preAddApps();
+        for (Callbacks cb : getCallbacks()) {
+            cb.preAddApps();
         }
         enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
     }
 
     public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) {
-        return new ModelWriter(mApp.getContext(), this, sBgDataModel,
+        return new ModelWriter(mApp.getContext(), this, mBgDataModel,
                 hasVerticalHotseat, verifyChanges);
     }
 
-    /**
-     * Set this as the current Launcher activity object for the loader.
-     */
-    public void initialize(Callbacks callbacks) {
-        synchronized (mLock) {
-            Preconditions.assertUIThread();
-            mCallbacks = new WeakReference<>(callbacks);
-        }
-    }
-
     @Override
     public void onPackageChanged(String packageName, UserHandle user) {
         int op = PackageUpdatedTask.OP_UPDATE;
         enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
     }
 
-    public void onSessionFailure(String packageName, UserHandle user) {
-        enqueueModelUpdateTask(new BaseModelUpdateTask() {
-            @Override
-            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-                final IntSparseArrayMap<Boolean> removedIds = new IntSparseArrayMap<>();
-                synchronized (dataModel) {
-                    for (ItemInfo info : dataModel.itemsIdMap) {
-                        if (info instanceof WorkspaceItemInfo
-                                && ((WorkspaceItemInfo) info).hasPromiseIconUi()
-                                && user.equals(info.user)
-                                && info.getIntent() != null
-                                && TextUtils.equals(packageName, info.getIntent().getPackage())) {
-                            removedIds.put(info.id, true /* remove */);
-                        }
-                    }
-                }
-
-                if (!removedIds.isEmpty()) {
-                    deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false));
-                }
-            }
-        });
-    }
-
     @Override
     public void onPackageRemoved(String packageName, UserHandle user) {
         onPackagesRemoved(user, packageName);
@@ -204,7 +156,7 @@
 
     public void onPackagesRemoved(UserHandle user, String... packages) {
         int op = PackageUpdatedTask.OP_REMOVE;
-        FileLog.d(TAG, "package removed received " + String.join("," + packages));
+        FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages));
         enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages));
     }
 
@@ -248,26 +200,29 @@
         enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
     }
 
-    public void updatePinnedShortcuts(String packageName, List<ShortcutInfo> shortcuts,
-            UserHandle user) {
-        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false));
+    /**
+     * Called when the icon for an app changes, outside of package event
+     */
+    @WorkerThread
+    public void onAppIconChanged(String packageName, UserHandle user) {
+        // Update the icon for the calendar package
+        Context context = mApp.getContext();
+        onPackageChanged(packageName, user);
+
+        List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
+                .forPackage(packageName).query(ShortcutRequest.PINNED);
+        if (!pinnedShortcuts.isEmpty()) {
+            enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
+                    false));
+        }
     }
 
-    /**
-     * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
-     * ACTION_PACKAGE_CHANGED.
-     */
-    @Override
-    public void onReceive(Context context, Intent intent) {
+    public void onBroadcastIntent(Intent intent) {
         if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
         final String action = intent.getAction();
         if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
             // If we have changed locale we need to clear out the labels in all apps/workspace.
             forceReload();
-        } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
-            UserManagerCompat.getInstance(context).enableAndResetCache();
-            forceReload();
         } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) ||
                 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) ||
                 Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) {
@@ -283,25 +238,24 @@
                 // we need to run the state change task again.
                 if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) ||
                         Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) {
-                    enqueueModelUpdateTask(new UserLockStateChangedTask(user));
+                    enqueueModelUpdateTask(new UserLockStateChangedTask(
+                            user, Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)));
                 }
             }
-        } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            Launcher l = (Launcher) getCallback();
-            l.reload();
+        } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
+            for (Callbacks cb : getCallbacks()) {
+                if (cb instanceof Launcher) {
+                    ((Launcher) cb).recreate();
+                }
+            }
         }
     }
 
-    public void forceReload() {
-        forceReload(-1);
-    }
-
     /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
-     * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
      */
-    public void forceReload(int synchronousBindPage) {
+    public void forceReload() {
         synchronized (mLock) {
             // Stop any existing loaders first, so they don't set mModelLoaded to true later
             stopLoader();
@@ -310,37 +264,77 @@
 
         // Start the loader if launcher is already running, otherwise the loader will run,
         // the next time launcher starts
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            if (synchronousBindPage < 0) {
-                synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
-            }
-            startLoader(synchronousBindPage);
+        if (hasCallbacks()) {
+            startLoader();
         }
     }
 
-    public boolean isCurrentCallbacks(Callbacks callbacks) {
-        return (mCallbacks != null && mCallbacks.get() == callbacks);
+    /**
+     * Rebinds all existing callbacks with already loaded model
+     */
+    public void rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader();
+        }
+    }
+
+    /**
+     * Removes an existing callback
+     */
+    public void removeCallbacks(Callbacks callbacks) {
+        synchronized (mCallbacksList) {
+            Preconditions.assertUIThread();
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader();
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     * @return true if workspace load was performed synchronously
+     */
+    public boolean addCallbacksAndLoad(Callbacks callbacks) {
+        synchronized (mLock) {
+            addCallbacks(callbacks);
+            return startLoader();
+
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     */
+    public void addCallbacks(Callbacks callbacks) {
+        Preconditions.assertUIThread();
+        synchronized (mCallbacksList) {
+            mCallbacksList.add(callbacks);
+        }
     }
 
     /**
      * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
      * @return true if the page could be bound synchronously.
      */
-    public boolean startLoader(int synchronousBindPage) {
+    public boolean startLoader() {
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
         synchronized (mLock) {
             // Don't bother to start the thread if we know it's not going to do anything
-            if (mCallbacks != null && mCallbacks.get() != null) {
-                final Callbacks oldCallbacks = mCallbacks.get();
+            final Callbacks[] callbacksList = getCallbacks();
+            if (callbacksList.length > 0) {
                 // Clear any pending bind-runnables from the synchronized load process.
-                MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+                for (Callbacks cb : callbacksList) {
+                    mMainExecutor.execute(cb::clearPendingBinds);
+                }
 
                 // If there is already one running, tell it to stop.
                 stopLoader();
-                LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
-                        mBgAllAppsList, synchronousBindPage, mCallbacks);
+                LoaderResults loaderResults = new LoaderResults(
+                        mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
                 if (mModelLoaded && !mIsLoaderTaskRunning) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -361,21 +355,24 @@
 
     /**
      * If there is already a loader task running, tell it to stop.
+     * @return true if an existing loader was stopped.
      */
-    public void stopLoader() {
+    public boolean stopLoader() {
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
             mLoaderTask = null;
             if (oldTask != null) {
                 oldTask.stopLocked();
+                return true;
             }
+            return false;
         }
     }
 
     public void startLoaderForResults(LoaderResults results) {
         synchronized (mLock) {
             stopLoader();
-            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
+            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
@@ -392,16 +389,65 @@
         }
     }
 
+    @Override
     public void onInstallSessionCreated(final PackageInstallInfo sessionInfo) {
+        if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
+            enqueueModelUpdateTask(new BaseModelUpdateTask() {
+                @Override
+                public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                    apps.addPromiseApp(app.getContext(), sessionInfo);
+                    bindApplicationsIfNeeded();
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onSessionFailure(String packageName, UserHandle user) {
+        if (!FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()) {
+            return;
+        }
         enqueueModelUpdateTask(new BaseModelUpdateTask() {
             @Override
             public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-                apps.addPromiseApp(app.getContext(), sessionInfo);
-                bindApplicationsIfNeeded();
+                final IntSparseArrayMap<Boolean> removedIds = new IntSparseArrayMap<>();
+                synchronized (dataModel) {
+                    for (ItemInfo info : dataModel.itemsIdMap) {
+                        if (info instanceof WorkspaceItemInfo
+                                && ((WorkspaceItemInfo) info).hasPromiseIconUi()
+                                && user.equals(info.user)
+                                && info.getIntent() != null
+                                && TextUtils.equals(packageName, info.getIntent().getPackage())) {
+                            removedIds.put(info.id, true /* remove */);
+                        }
+                    }
+                }
+
+                if (!removedIds.isEmpty()) {
+                    deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false));
+                }
             }
         });
     }
 
+    @Override
+    public void onPackageStateChanged(PackageInstallInfo installInfo) {
+        enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo));
+    }
+
+    /**
+     * Updates the icons and label of all pending icons for the provided package name.
+     */
+    @Override
+    public void onUpdateSessionDisplay(PackageUserKey key, PackageInstaller.SessionInfo info) {
+        mApp.getIconCache().updateSessionCache(key, info);
+
+        HashSet<String> packages = new HashSet<>();
+        packages.add(key.mPackageName);
+        enqueueModelUpdateTask(new CacheDataUpdatedTask(
+                CacheDataUpdatedTask.OP_SESSION_UPDATE, key.mUser, packages));
+    }
+
     public class LoaderTransaction implements AutoCloseable {
 
         private final LoaderTask mTask;
@@ -443,7 +489,7 @@
     /**
      * Refreshes the cached shortcuts if the shortcut permission has changed.
      * Current implementation simply reloads the workspace, but it can be optimized to
-     * use partial updates similar to {@link UserManagerCompat}
+     * use partial updates similar to {@link UserCache}
      */
     public void refreshShortcutsIfRequired() {
         MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable);
@@ -474,7 +520,7 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
         MODEL_EXECUTOR.execute(task);
     }
 
@@ -503,9 +549,7 @@
     public void updateAndBindWorkspaceItem(WorkspaceItemInfo si, ShortcutInfo info) {
         updateAndBindWorkspaceItem(() -> {
             si.updateFromDeepShortcutInfo(info, mApp.getContext());
-            LauncherIcons li = LauncherIcons.obtain(mApp.getContext());
-            si.applyFrom(li.createShortcutIcon(info));
-            li.recycle();
+            mApp.getIconCache().getShortcutIcon(si, info);
             return si;
         });
     }
@@ -540,14 +584,29 @@
         if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
             writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size());
             for (AppInfo info : mBgAllAppsList.data) {
-                writer.println(prefix + "   title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap
+                writer.println(prefix + "   title=\"" + info.title
+                        + "\" bitmapIcon=" + info.bitmap.icon
                         + " componentName=" + info.componentName.getPackageName());
             }
         }
-        sBgDataModel.dump(prefix, fd, writer, args);
+        mBgDataModel.dump(prefix, fd, writer, args);
     }
 
-    public Callbacks getCallback() {
-        return mCallbacks != null ? mCallbacks.get() : null;
+    /**
+     * Returns true if there are any callbacks attached to the model
+     */
+    public boolean hasCallbacks() {
+        synchronized (mCallbacksList) {
+            return !mCallbacksList.isEmpty();
+        }
+    }
+
+    /**
+     * Returns an array of currently attached callbacks
+     */
+    public Callbacks[] getCallbacks() {
+        synchronized (mCallbacksList) {
+            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6081300..e8b5568 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 
@@ -45,10 +47,9 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.BaseColumns;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -57,10 +58,10 @@
 
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DbDowngradeHelper;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -69,7 +70,6 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -83,12 +83,15 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
     private static final boolean LOGD = false;
 
     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
+    private static final long RESTORE_BACKUP_TABLE_DELAY = TimeUnit.SECONDS.toMillis(30);
 
     /**
      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
@@ -100,11 +103,10 @@
 
     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
-    private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
-    private Handler mListenerHandler;
-
     protected DatabaseHelper mOpenHelper;
 
+    private long mLastRestoreTimestamp = 0L;
+
     /**
      * $ adb shell dumpsys activity provider com.android.launcher3
      */
@@ -119,10 +121,9 @@
 
     @Override
     public boolean onCreate() {
-        if (FeatureFlags.IS_DOGFOOD_BUILD) {
+        if (FeatureFlags.IS_STUDIO_BUILD) {
             Log.d(TAG, "Launcher process started");
         }
-        mListenerHandler = new Handler(mListenerWrapper);
 
         // The content provider exists for the entire duration of the launcher main process and
         // is the first component to get created.
@@ -130,14 +131,6 @@
         return true;
     }
 
-    /**
-     * Sets a provider listener.
-     */
-    public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
-        Preconditions.assertUIThread();
-        mListenerWrapper.mListener = listener;
-    }
-
     @Override
     public String getType(Uri uri) {
         SqlArguments args = new SqlArguments(uri, null, null);
@@ -153,7 +146,8 @@
      */
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
+            mOpenHelper = DatabaseHelper.createDatabaseHelper(
+                    getContext(), false /* forMigration */);
 
             if (RestoreDbTask.isPending(getContext())) {
                 if (!RestoreDbTask.performRestore(getContext(), mOpenHelper,
@@ -167,6 +161,20 @@
         }
     }
 
+    private synchronized boolean prepForMigration(String dbFile, String targetTableName,
+            Supplier<DatabaseHelper> src, Supplier<DatabaseHelper> dst) {
+        if (TextUtils.equals(dbFile, mOpenHelper.getDatabaseName())) {
+            return false;
+        }
+
+        final DatabaseHelper helper = src.get();
+        mOpenHelper = dst.get();
+        copyTable(helper.getReadableDatabase(), Favorites.TABLE_NAME,
+                mOpenHelper.getWritableDatabase(), targetTableName, getContext());
+        helper.close();
+        return true;
+    }
+
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
@@ -220,10 +228,9 @@
         addModifiedTime(initialValues);
         final int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
         if (rowId < 0) return null;
-        mOpenHelper.onAddOrDeleteOp(db);
+        onAddOrDeleteOp(db);
 
         uri = ContentUris.withAppendedId(uri, rowId);
-        notifyListeners();
         reloadLauncherIfExternal();
         return uri;
     }
@@ -279,11 +286,10 @@
                     return 0;
                 }
             }
-            mOpenHelper.onAddOrDeleteOp(db);
+            onAddOrDeleteOp(db);
             t.commit();
         }
 
-        notifyListeners();
         reloadLauncherIfExternal();
         return values.length;
     }
@@ -306,7 +312,7 @@
                         results[i].count != null && results[i].count > 0;
             }
             if (isAddOrDelete) {
-                mOpenHelper.onAddOrDeleteOp(t.getDb());
+                onAddOrDeleteOp(t.getDb());
             }
 
             t.commit();
@@ -328,8 +334,7 @@
         }
         int count = db.delete(args.table, args.where, args.args);
         if (count > 0) {
-            mOpenHelper.onAddOrDeleteOp(db);
-            notifyListeners();
+            onAddOrDeleteOp(db);
             reloadLauncherIfExternal();
         }
         return count;
@@ -343,8 +348,6 @@
         addModifiedTime(values);
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count = db.update(args.table, values, args.where, args.args);
-        if (count > 0) notifyListeners();
-
         reloadLauncherIfExternal();
         return count;
     }
@@ -375,12 +378,14 @@
             }
             case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
                 Bundle result = new Bundle();
-                result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId());
+                result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
+                        mOpenHelper.generateNewItemId());
                 return result;
             }
             case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
                 Bundle result = new Bundle();
-                result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId());
+                result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
+                        mOpenHelper.generateNewScreenId());
                 return result;
             }
             case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
@@ -402,14 +407,58 @@
                 return result;
             }
             case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
-                mOpenHelper.mBackupTableExists =
-                        tableExists(mOpenHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
+                mOpenHelper.mBackupTableExists = tableExists(mOpenHelper.getReadableDatabase(),
+                        Favorites.BACKUP_TABLE_NAME);
                 return null;
             }
+            case LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE: {
+                mOpenHelper.mHotseatRestoreTableExists = tableExists(
+                        mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+                return null;
+            }
+            case LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE: {
+                final long ts = System.currentTimeMillis();
+                if (ts - mLastRestoreTimestamp > RESTORE_BACKUP_TABLE_DELAY) {
+                    mLastRestoreTimestamp = ts;
+                    RestoreDbTask.restoreIfPossible(
+                            getContext(), mOpenHelper, new BackupManager(getContext()));
+                }
+                return null;
+            }
+            case LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER: {
+                if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
+                    Bundle result = new Bundle();
+                    result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
+                            prepForMigration(
+                                    InvariantDeviceProfile.INSTANCE.get(getContext()).dbFile,
+                                    Favorites.TMP_TABLE,
+                                    () -> mOpenHelper,
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), true /* forMigration */)));
+                    return result;
+                }
+            }
+            case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
+                if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
+                    Bundle result = new Bundle();
+                    result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
+                            prepForMigration(
+                                    arg /* dbFile */,
+                                    Favorites.PREVIEW_TABLE_NAME,
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), arg, true /* forMigration */),
+                                    () -> mOpenHelper));
+                    return result;
+                }
+            }
         }
         return null;
     }
 
+    private void onAddOrDeleteOp(SQLiteDatabase db) {
+        mOpenHelper.onAddOrDeleteOp(db);
+    }
+
     /**
      * Deletes any empty folder from the DB.
      * @return Ids of deleted folders.
@@ -438,13 +487,6 @@
         }
     }
 
-    /**
-     * Overridden in tests
-     */
-    protected void notifyListeners() {
-        mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
-    }
-
     @Thunk static void addModifiedTime(ContentValues values) {
         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
     }
@@ -511,8 +553,6 @@
      */
     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
         Context ctx = getContext();
-        InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
-
         String authority = Settings.Secure.getString(ctx.getContentResolver(),
                 "launcher3.layout.provider");
         if (TextUtils.isEmpty(authority)) {
@@ -524,13 +564,7 @@
             Log.e(TAG, "No provider found for authority " + authority);
             return null;
         }
-        Uri uri = new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
-                .appendQueryParameter("version", "1")
-                .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
-                .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
-                .appendQueryParameter("hotseatSize", Integer.toString(grid.numHotseatIcons))
-                .build();
-
+        Uri uri = getLayoutUri(authority, ctx);
         try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
             // Read the full xml so that we fail early in case of any IO error.
             String layout = new String(IOUtils.toByteArray(in));
@@ -547,12 +581,22 @@
         }
     }
 
+    public static Uri getLayoutUri(String authority, Context ctx) {
+        InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
+        return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
+                .appendQueryParameter("version", "1")
+                .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
+                .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
+                .appendQueryParameter("hotseatSize", Integer.toString(grid.numHotseatIcons))
+                .build();
+    }
+
     private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
         InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
         int defaultLayout = idp.defaultLayoutId;
 
-        UserManagerCompat um = UserManagerCompat.getInstance(getContext());
-        if (um.isDemoUser() && idp.demoModeLayoutId != 0) {
+        if (getContext().getSystemService(UserManager.class).isDemoUser()
+                && idp.demoModeLayoutId != 0) {
             defaultLayout = idp.demoModeLayoutId;
         }
 
@@ -563,38 +607,52 @@
     /**
      * The class is subclassed in tests to create an in-memory db.
      */
-    public static class DatabaseHelper extends NoLocaleSQLiteHelper implements LayoutParserCallback {
-        private final BackupManager mBackupManager;
-        private final Handler mWidgetHostResetHandler;
+    public static class DatabaseHelper extends NoLocaleSQLiteHelper implements
+            LayoutParserCallback {
         private final Context mContext;
+        private final boolean mForMigration;
         private int mMaxItemId = -1;
         private int mMaxScreenId = -1;
         private boolean mBackupTableExists;
+        private boolean mHotseatRestoreTableExists;
 
-        DatabaseHelper(Context context, Handler widgetHostResetHandler) {
-            this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
+        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
+            return createDatabaseHelper(context, null, forMigration);
+        }
+
+        static DatabaseHelper createDatabaseHelper(Context context, String dbName,
+                boolean forMigration) {
+            if (dbName == null) {
+                dbName = MULTI_DB_GRID_MIRATION_ALGO.get() ? InvariantDeviceProfile.INSTANCE.get(
+                        context).dbFile : LauncherFiles.LAUNCHER_DB;
+            }
+            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
             // Table creation sometimes fails silently, which leads to a crash loop.
             // This way, we will try to create a table every time after crash, so the device
             // would eventually be able to recover.
-            if (!tableExists(getReadableDatabase(), Favorites.TABLE_NAME)) {
+            if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
                 // This operation is a no-op if the table already exists.
-                addFavoritesTable(getWritableDatabase(), true);
+                databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
             }
-            mBackupTableExists = tableExists(getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
+            if (!MULTI_DB_GRID_MIRATION_ALGO.get()) {
+                databaseHelper.mBackupTableExists = tableExists(
+                        databaseHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
+            }
+            databaseHelper.mHotseatRestoreTableExists = tableExists(
+                    databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
 
-            initIds();
+            databaseHelper.initIds();
+            return databaseHelper;
         }
 
         /**
          * Constructor used in tests and for restore.
          */
-        public DatabaseHelper(
-                Context context, Handler widgetHostResetHandler, String tableName) {
-            super(context, tableName, SCHEMA_VERSION);
+        public DatabaseHelper(Context context, String dbName, boolean forMigration) {
+            super(context, dbName, SCHEMA_VERSION);
             mContext = context;
-            mWidgetHostResetHandler = widgetHostResetHandler;
-            mBackupManager = new BackupManager(mContext);
+            mForMigration = forMigration;
         }
 
         protected void initIds() {
@@ -619,7 +677,9 @@
 
             // Fresh and clean launcher DB.
             mMaxItemId = initializeMaxItemId(db);
-            onEmptyDbCreated();
+            if (!mForMigration) {
+                onEmptyDbCreated();
+            }
         }
 
         protected void onAddOrDeleteOp(SQLiteDatabase db) {
@@ -627,25 +687,22 @@
                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
                 mBackupTableExists = false;
             }
+            if (mHotseatRestoreTableExists) {
+                dropTable(db, Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+                mHotseatRestoreTableExists = false;
+            }
         }
 
         /**
          * Overriden in tests.
          */
         protected void onEmptyDbCreated() {
-            // Database was just created, so wipe any previous widgets
-            if (mWidgetHostResetHandler != null) {
-                newLauncherWidgetHost().deleteHost();
-                mWidgetHostResetHandler.sendEmptyMessage(
-                        ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
-            }
-
             // Set the flag for empty DB
             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
         }
 
         public long getSerialNumberForUser(UserHandle user) {
-            return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user);
+            return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
         }
 
         public long getDefaultUserSerial() {
@@ -676,7 +733,7 @@
          */
         protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
             // Remove "profile extra"
-            UserManagerCompat um = UserManagerCompat.getInstance(mContext);
+            UserCache um = UserCache.INSTANCE.get(mContext);
             for (UserHandle user : um.getUserProfiles()) {
                 long serial = um.getSerialNumberForUser(user);
                 String sql = "update favorites set intent = replace(intent, "
@@ -1039,27 +1096,4 @@
             }
         }
     }
-
-    private static class ChangeListenerWrapper implements Handler.Callback {
-
-        private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
-        private static final int MSG_APP_WIDGET_HOST_RESET = 2;
-
-        private LauncherProviderChangeListener mListener;
-
-        @Override
-        public boolean handleMessage(Message msg) {
-            if (mListener != null) {
-                switch (msg.what) {
-                    case MSG_LAUNCHER_PROVIDER_CHANGED:
-                        mListener.onLauncherProviderChanged();
-                        break;
-                    case MSG_APP_WIDGET_HOST_RESET:
-                        mListener.onAppWidgetHostReset();
-                        break;
-                }
-            }
-            return true;
-        }
-    }
 }
diff --git a/src/com/android/launcher3/LauncherProviderChangeListener.java b/src/com/android/launcher3/LauncherProviderChangeListener.java
deleted file mode 100644
index 0243088..0000000
--- a/src/com/android/launcher3/LauncherProviderChangeListener.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.android.launcher3;
-
-/**
- * This class is a listener for {@link LauncherProvider} changes. It gets notified in the
- * sendNotify method. This listener is needed because by default the Launcher suppresses
- * standard data change callbacks.
- */
-public interface LauncherProviderChangeListener {
-
-    void onLauncherProviderChanged();
-
-    void onAppWidgetHostReset();
-}
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index f964b8d..51504ce 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -1,22 +1,17 @@
 package com.android.launcher3;
 
-import static com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_ROOT_VIEW;
+import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
 
 import android.annotation.TargetApi;
-import android.app.ActivityManager;
 import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Paint;
 import android.graphics.Rect;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.view.View;
 import android.view.ViewDebug;
 import android.view.WindowInsets;
 
+import com.android.launcher3.statemanager.StatefulActivity;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -24,82 +19,31 @@
 
     private final Rect mTempRect = new Rect();
 
-    private final Launcher mLauncher;
-
-    private final Paint mOpaquePaint;
-
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private final Rect mConsumedInsets = new Rect();
+    private final StatefulActivity mActivity;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
 
-    private View mAlignedView;
     private WindowStateListener mWindowStateListener;
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisallowBackGesture;
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private boolean mForceHideBackArrow;
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
-
-        mOpaquePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mOpaquePaint.setColor(Color.BLACK);
-        mOpaquePaint.setStyle(Paint.Style.FILL);
-
-        mLauncher = Launcher.getLauncher(context);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        if (getChildCount() > 0) {
-            // LauncherRootView contains only one child, which should be aligned
-            // based on the horizontal insets.
-            mAlignedView = getChildAt(0);
-        }
-        super.onFinishInflate();
+        mActivity = StatefulActivity.fromContext(context);
     }
 
     private void handleSystemWindowInsets(Rect insets) {
-        mConsumedInsets.setEmpty();
-        boolean drawInsetBar = false;
-        if (mLauncher.isInMultiWindowMode()
-                && (insets.left > 0 || insets.right > 0 || insets.bottom > 0)) {
-            mConsumedInsets.left = insets.left;
-            mConsumedInsets.right = insets.right;
-            mConsumedInsets.bottom = insets.bottom;
-            insets.set(0, insets.top, 0, 0);
-            drawInsetBar = true;
-        } else  if ((insets.right > 0 || insets.left > 0) &&
-                getContext().getSystemService(ActivityManager.class).isLowRamDevice()) {
-            mConsumedInsets.left = insets.left;
-            mConsumedInsets.right = insets.right;
-            insets.set(0, insets.top, 0, insets.bottom);
-            drawInsetBar = true;
-        }
-
-        mLauncher.getSystemUiController().updateUiState(
-                UI_STATE_ROOT_VIEW, drawInsetBar ? FLAG_DARK_NAV : 0);
-
         // Update device profile before notifying th children.
-        mLauncher.updateInsets(insets);
+        mActivity.getDeviceProfile().updateInsets(insets);
         boolean resetState = !insets.equals(mInsets);
         setInsets(insets);
 
-        if (mAlignedView != null) {
-            // Apply margins on aligned view to handle consumed insets.
-            MarginLayoutParams lp = (MarginLayoutParams) mAlignedView.getLayoutParams();
-            if (lp.leftMargin != mConsumedInsets.left || lp.rightMargin != mConsumedInsets.right ||
-                    lp.bottomMargin != mConsumedInsets.bottom) {
-                lp.leftMargin = mConsumedInsets.left;
-                lp.rightMargin = mConsumedInsets.right;
-                lp.topMargin = mConsumedInsets.top;
-                lp.bottomMargin = mConsumedInsets.bottom;
-                mAlignedView.setLayoutParams(lp);
-            }
-        }
         if (resetState) {
-            mLauncher.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
+            mActivity.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
         }
     }
 
@@ -108,12 +52,7 @@
         mTempRect.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
                 insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
         handleSystemWindowInsets(mTempRect);
-        if (Utilities.ATLEAST_Q) {
-            return insets.inset(mConsumedInsets.left, mConsumedInsets.top,
-                    mConsumedInsets.right, mConsumedInsets.bottom);
-        } else {
-            return insets.replaceSystemWindowInsets(mTempRect);
-        }
+        return insets;
     }
 
     @Override
@@ -126,28 +65,10 @@
     }
 
     public void dispatchInsets() {
-        mLauncher.updateInsets(mInsets);
+        mActivity.getDeviceProfile().updateInsets(mInsets);
         super.setInsets(mInsets);
     }
 
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        super.dispatchDraw(canvas);
-
-        // If the right inset is opaque, draw a black rectangle to ensure that is stays opaque.
-        if (mConsumedInsets.right > 0) {
-            int width = getWidth();
-            canvas.drawRect(width - mConsumedInsets.right, 0, width, getHeight(), mOpaquePaint);
-        }
-        if (mConsumedInsets.left > 0) {
-            canvas.drawRect(0, 0, mConsumedInsets.left, getHeight(), mOpaquePaint);
-        }
-        if (mConsumedInsets.bottom > 0) {
-            int height = getHeight();
-            canvas.drawRect(0, height - mConsumedInsets.bottom, getWidth(), height, mOpaquePaint);
-        }
-    }
-
     public void setWindowStateListener(WindowStateListener listener) {
         mWindowStateListener = listener;
     }
@@ -176,12 +97,18 @@
     }
 
     @TargetApi(Build.VERSION_CODES.Q)
+    public void setForceHideBackArrow(boolean forceHideBackArrow) {
+        this.mForceHideBackArrow = forceHideBackArrow;
+        setDisallowBackGesture(mDisallowBackGesture);
+    }
+
+    @TargetApi(Build.VERSION_CODES.Q)
     public void setDisallowBackGesture(boolean disallowBackGesture) {
-        if (!Utilities.ATLEAST_Q) {
+        if (!Utilities.ATLEAST_Q || SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
         mDisallowBackGesture = disallowBackGesture;
-        setSystemGestureExclusionRects(mDisallowBackGesture
+        setSystemGestureExclusionRects((mForceHideBackArrow || mDisallowBackGesture)
                 ? SYSTEM_GESTURE_EXCLUSION_RECT
                 : Collections.emptyList());
     }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index c509680..5512654 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -22,6 +22,8 @@
 import android.os.Bundle;
 import android.provider.BaseColumns;
 
+import com.android.launcher3.model.data.ItemInfo;
+
 /**
  * Settings related utilities.
  */
@@ -93,15 +95,42 @@
         public static final String TABLE_NAME = "favorites";
 
         /**
-         * Backup table created when when the favorites table is modified during grid migration
+         * Backup table created when the favorites table is modified during grid migration
          */
         public static final String BACKUP_TABLE_NAME = "favorites_bakup";
 
         /**
-         * The content:// style URL for this table
+         * Backup table created when user hotseat is moved to workspace for hybrid hotseat
          */
-        public static final Uri CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
+        public static final String HYBRID_HOTSEAT_BACKUP_TABLE = "hotseat_restore_backup";
+
+        /**
+         * Temporary table used specifically for grid migrations during wallpaper preview
+         */
+        public static final String PREVIEW_TABLE_NAME = "favorites_preview";
+
+        /**
+         * Temporary table used specifically for multi-db grid migrations
+         */
+        public static final String TMP_TABLE = "favorites_tmp";
+
+        /**
+         * The content:// style URL for "favorites" table
+         */
+        public static final Uri CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
+
+        /**
+         * The content:// style URL for "favorites_preview" table
+         */
+        public static final Uri PREVIEW_CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + PREVIEW_TABLE_NAME);
+
+        /**
+         * The content:// style URL for "favorites_tmp" table
+         */
+        public static final Uri TMP_CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + TMP_TABLE);
 
         /**
          * The content:// style URL for a given row, identified by its id.
@@ -111,8 +140,8 @@
          * @return The unique content URL for the specified row.
          */
         public static Uri getContentUri(int id) {
-            return Uri.parse("content://" + LauncherProvider.AUTHORITY +
-                    "/" + TABLE_NAME + "/" + id);
+            return Uri.parse("content://" + LauncherProvider.AUTHORITY
+                    + "/" + TABLE_NAME + "/" + id);
         }
 
         /**
@@ -127,17 +156,29 @@
         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_HOTSEAT_PREDICTION = -103;
+        public static final int CONTAINER_ALL_APPS = -104;
+        public static final int CONTAINER_WIDGETS_TRAY = -105;
+        // Represents search results view.
+        public static final int CONTAINER_SEARCH_RESULTS = -106;
+        public static final int CONTAINER_SHORTCUTS = -107;
+        public static final int CONTAINER_SETTINGS = -108;
+        public static final int CONTAINER_TASKSWITCHER = -109;
 
-        static final String containerToString(int container) {
+        public static final String containerToString(int container) {
             switch (container) {
                 case CONTAINER_DESKTOP: return "desktop";
                 case CONTAINER_HOTSEAT: return "hotseat";
                 case CONTAINER_PREDICTION: return "prediction";
+                case CONTAINER_ALL_APPS: return "all_apps";
+                case CONTAINER_WIDGETS_TRAY: return "widgets_tray";
+                case CONTAINER_SEARCH_RESULTS: return "search_result";
+                case CONTAINER_SHORTCUTS: return "shortcuts";
                 default: return String.valueOf(container);
             }
         }
 
-        static final String itemTypeToString(int type) {
+        public static final String itemTypeToString(int type) {
             switch(type) {
                 case ITEM_TYPE_APPLICATION: return "APP";
                 case ITEM_TYPE_SHORTCUT: return "SHORTCUT";
@@ -210,6 +251,12 @@
         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>
@@ -299,10 +346,22 @@
 
         public static final String METHOD_REFRESH_BACKUP_TABLE = "refresh_backup_table";
 
+        public static final String METHOD_REFRESH_HOTSEAT_RESTORE_TABLE = "restore_hotseat_table";
+
+        public static final String METHOD_RESTORE_BACKUP_TABLE = "restore_backup_table";
+
+        public static final String METHOD_UPDATE_CURRENT_OPEN_HELPER = "update_current_open_helper";
+
+        public static final String METHOD_PREP_FOR_PREVIEW = "prep_for_preview";
+
         public static final String EXTRA_VALUE = "value";
 
         public static Bundle call(ContentResolver cr, String method) {
-            return cr.call(CONTENT_URI, method, null, null);
+            return call(cr, method, null);
+        }
+
+        public static Bundle call(ContentResolver cr, String method, String arg) {
+            return cr.call(CONTENT_URI, method, arg, null);
         }
     }
 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 6e2626b..c78df62 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -15,47 +15,35 @@
  */
 package com.android.launcher3;
 
-import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
-import static android.view.View.VISIBLE;
-import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_PEEK_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
+import android.content.Context;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.HintState;
 import com.android.launcher3.states.SpringLoadedState;
-import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.uioverrides.states.AllAppsState;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 import java.util.Arrays;
 
-
 /**
  * Base state for various states used for the Launcher
  */
-public class LauncherState {
-
+public abstract class LauncherState implements BaseState<LauncherState> {
 
     /**
      * Set of elements indicating various workspace elements which change visibility across states
@@ -68,18 +56,33 @@
     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 RECENTS_CLEAR_ALL_BUTTON = 1 << 6;
+    public static final int OVERVIEW_BUTTONS = 1 << 6;
 
-    protected static final int FLAG_MULTI_PAGE = 1 << 0;
-    protected static final int FLAG_DISABLE_ACCESSIBILITY = 1 << 1;
-    protected static final int FLAG_DISABLE_RESTORE = 1 << 2;
-    protected static final int FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED = 1 << 3;
-    protected static final int FLAG_DISABLE_PAGE_CLIPPING = 1 << 4;
-    protected static final int FLAG_PAGE_BACKGROUNDS = 1 << 5;
-    protected static final int FLAG_DISABLE_INTERACTION = 1 << 6;
-    protected static final int FLAG_OVERVIEW_UI = 1 << 7;
-    protected static final int FLAG_HIDE_BACK_BUTTON = 1 << 8;
-    protected static final int FLAG_HAS_SYS_UI_SCRIM = 1 << 9;
+    /** Mask of all the items that are contained in the apps view. */
+    public static final int APPS_VIEW_ITEM_MASK =
+            HOTSEAT_SEARCH_BOX | ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT;
+
+    // Flag indicating workspace has multiple pages visible.
+    public static final int FLAG_MULTI_PAGE = BaseState.getFlag(0);
+    // Flag indicating that workspace and its contents are not accessible
+    public static final int FLAG_WORKSPACE_INACCESSIBLE = BaseState.getFlag(1);
+
+    // Flag indicating the state allows workspace icons to be dragged.
+    public static final int FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED = BaseState.getFlag(2);
+    // Flag to indicate that workspace should draw page background
+    public static final int FLAG_WORKSPACE_HAS_BACKGROUNDS = BaseState.getFlag(3);
+    // True if the back button should be hidden when in this state (assuming no floating views are
+    // open, launcher has window focus, etc).
+    public static final int FLAG_HIDE_BACK_BUTTON = BaseState.getFlag(4);
+    // Flag to indicate if the state would have scrim over sysui region: statu sbar and nav bar
+    public static final int FLAG_HAS_SYS_UI_SCRIM = BaseState.getFlag(5);
+    // Flag to inticate that all popups should be closed when this state is enabled.
+    public static final int FLAG_CLOSE_POPUPS = BaseState.getFlag(6);
+    public static final int FLAG_OVERVIEW_UI = BaseState.getFlag(7);
+
+
+    public static final float NO_OFFSET = 0;
+    public static final float NO_SCALE = 1;
 
     protected static final PageAlphaProvider DEFAULT_ALPHA_PROVIDER =
             new PageAlphaProvider(ACCEL_2) {
@@ -89,15 +92,21 @@
                 }
             };
 
-    private static final LauncherState[] sAllStates = new LauncherState[7];
+    private static final LauncherState[] sAllStates = new LauncherState[9];
 
     /**
      * TODO: Create a separate class for NORMAL state.
      */
     public static final LauncherState NORMAL = new LauncherState(NORMAL_STATE_ORDINAL,
-            ContainerType.WORKSPACE, 0,
+            ContainerType.WORKSPACE,
             FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_HIDE_BACK_BUTTON |
-            FLAG_HAS_SYS_UI_SCRIM);
+                    FLAG_HAS_SYS_UI_SCRIM) {
+        @Override
+        public int getTransitionDuration(Context context) {
+            // Arbitrary duration, when going to NORMAL we use the state we're coming from instead.
+            return 0;
+        }
+    };
 
     /**
      * Various Launcher states arranged in the increasing order of UI layers
@@ -105,10 +114,13 @@
     public static final LauncherState SPRING_LOADED = new SpringLoadedState(
             SPRING_LOADED_STATE_ORDINAL);
     public static final LauncherState ALL_APPS = new AllAppsState(ALL_APPS_STATE_ORDINAL);
+    public static final LauncherState HINT_STATE = new HintState(HINT_STATE_ORDINAL);
 
     public static final LauncherState OVERVIEW = new OverviewState(OVERVIEW_STATE_ORDINAL);
     public static final LauncherState OVERVIEW_PEEK =
             OverviewState.newPeekState(OVERVIEW_PEEK_STATE_ORDINAL);
+    public static final LauncherState OVERVIEW_MODAL_TASK = OverviewState.newModalTaskState(
+            OVERVIEW_MODAL_TASK_STATE_ORDINAL);
     public static final LauncherState QUICK_SWITCH =
             OverviewState.newSwitchState(QUICK_SWITCH_STATE_ORDINAL);
     public static final LauncherState BACKGROUND_APP =
@@ -122,86 +134,34 @@
     public final int containerType;
 
     /**
-     * True if the state can be persisted across activity restarts.
-     */
-    public final boolean disableRestore;
-
-    /**
-     * True if workspace has multiple pages visible.
-     */
-    public final boolean hasMultipleVisiblePages;
-
-    /**
-     * Accessibility flag for workspace and its pages.
-     * @see android.view.View#setImportantForAccessibility(int)
-     */
-    public final int workspaceAccessibilityFlag;
-
-    /**
-     * Properties related to state transition animation
-     *
-     * @see WorkspaceStateTransitionAnimation
-     */
-    public final boolean hasWorkspacePageBackground;
-
-    public final int transitionDuration;
-
-    /**
-     * True if the state allows workspace icons to be dragged.
-     */
-    public final boolean workspaceIconsCanBeDragged;
-
-    /**
-     * True if the workspace pages should not be clipped relative to the workspace bounds
-     * for this state.
-     */
-    public final boolean disablePageClipping;
-
-    /**
-     * True if launcher can not be directly interacted in this state;
-     */
-    public final boolean disableInteraction;
-
-    /**
      * True if the state has overview panel visible.
      */
     public final boolean overviewUi;
 
-    /**
-     * True if the back button should be hidden when in this state (assuming no floating views are
-     * open, launcher has window focus, etc).
-     */
-    public final boolean hideBackButton;
+    private final int mFlags;
 
-    public final boolean hasSysUiScrim;
-
-    public LauncherState(int id, int containerType, int transitionDuration, int flags) {
+    public LauncherState(int id, int containerType, int flags) {
         this.containerType = containerType;
-        this.transitionDuration = transitionDuration;
-
-        this.hasWorkspacePageBackground = (flags & FLAG_PAGE_BACKGROUNDS) != 0;
-        this.hasMultipleVisiblePages = (flags & FLAG_MULTI_PAGE) != 0;
-        this.workspaceAccessibilityFlag = (flags & FLAG_DISABLE_ACCESSIBILITY) != 0
-                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-        this.disableRestore = (flags & FLAG_DISABLE_RESTORE) != 0;
-        this.workspaceIconsCanBeDragged = (flags & FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED) != 0;
-        this.disablePageClipping = (flags & FLAG_DISABLE_PAGE_CLIPPING) != 0;
-        this.disableInteraction = (flags & FLAG_DISABLE_INTERACTION) != 0;
+        this.mFlags = flags;
         this.overviewUi = (flags & FLAG_OVERVIEW_UI) != 0;
-        this.hideBackButton = (flags & FLAG_HIDE_BACK_BUTTON) != 0;
-        this.hasSysUiScrim = (flags & FLAG_HAS_SYS_UI_SCRIM) != 0;
-
         this.ordinal = id;
         sAllStates[id] = this;
     }
 
+    /**
+     * Returns if the state has the provided flag
+     */
+    @Override
+    public final boolean hasFlag(int mask) {
+        return (mFlags & mask) != 0;
+    }
+
     public static LauncherState[] values() {
         return Arrays.copyOf(sAllStates, sAllStates.length);
     }
 
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1, 0, 0);
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
@@ -209,20 +169,24 @@
         return getWorkspaceScaleAndTranslation(launcher);
     }
 
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return UiFactory.getOverviewScaleAndTranslationForNormalState(launcher);
+    /**
+     * Returns an array of two elements.
+     *   The first specifies the scale for the overview
+     *   The second is the factor ([0, 1], 0 => center-screen; 1 => offscreen) by which overview
+     *   should be shifted horizontally.
+     */
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return launcher.getNormalOverviewScaleAndOffset();
+    }
+
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public float getOverviewFullscreenProgress() {
         return 0;
     }
 
-    public void onStateEnabled(Launcher launcher) {
-        dispatchWindowStateChanged(launcher);
-    }
-
-    public void onStateDisabled(Launcher launcher) { }
-
     public int getVisibleElements(Launcher launcher) {
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return HOTSEAT_ICONS | VERTICAL_SWIPE_INDICATOR;
@@ -247,6 +211,40 @@
         return 0;
     }
 
+    /**
+     * For this state, how modal should over view been shown. 0 modalness means all tasks drawn,
+     * 1 modalness means the current task is show on its own.
+     */
+    public float getOverviewModalness() {
+        return 0;
+    }
+
+    /**
+     * The amount of blur and wallpaper zoom to apply to the background of either the app
+     * or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
+     *
+     * 0 means completely zoomed in, without blurs. 1 is zoomed out, with blurs.
+     */
+    public final float getDepth(Context context) {
+        return getDepth(context,
+                BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode);
+    }
+
+    /**
+     * Returns the amount of blur and wallpaper zoom for this state with {@param isMultiWindowMode}.
+     * @see #getDepth(Context).
+     */
+    public final float getDepth(Context context, boolean isMultiWindowMode) {
+        if (isMultiWindowMode) {
+            return 0;
+        }
+        return getDepthUnchecked(context);
+    }
+
+    protected float getDepthUnchecked(Context context) {
+        return 0f;
+    }
+
     public String getDescription(Launcher launcher) {
         return launcher.getWorkspace().getCurrentPageDescription();
     }
@@ -264,73 +262,25 @@
         };
     }
 
+    @Override
     public LauncherState getHistoryForState(LauncherState previousState) {
         // No history is supported
         return NORMAL;
     }
 
-    /**
-     * Called when the start transition ends and the user settles on this particular state.
-     */
-    public void onStateTransitionEnd(Launcher launcher) {
-        if (this == NORMAL) {
-            // Clear any rotation locks when going to normal state
-            launcher.getRotationHelper().setCurrentStateRequest(REQUEST_NONE);
-        }
+    @Override
+    public String toString() {
+        return TestProtocol.stateOrdinalToString(ordinal);
     }
 
     public void onBackPressed(Launcher launcher) {
         if (this != NORMAL) {
-            LauncherStateManager lsm = launcher.getStateManager();
+            StateManager<LauncherState> lsm = launcher.getStateManager();
             LauncherState lastState = lsm.getLastState();
             lsm.goToState(lastState);
         }
     }
 
-    /**
-     * Prepares for a non-user controlled animation from fromState to this state. Preparations
-     * include:
-     * - Setting interpolators for various animations included in the state transition.
-     * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
-     */
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            AnimatorSetBuilder builder) {
-        if (this == NORMAL && fromState == OVERVIEW) {
-            builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
-            builder.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
-            builder.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL);
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
-            Workspace workspace = launcher.getWorkspace();
-
-            // Start from a higher workspace scale, but only if we're invisible so we don't jump.
-            boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
-            if (isWorkspaceVisible) {
-                CellLayout currentChild = (CellLayout) workspace.getChildAt(
-                        workspace.getCurrentPage());
-                isWorkspaceVisible = currentChild.getVisibility() == VISIBLE
-                        && currentChild.getShortcutsAndWidgets().getAlpha() > 0;
-            }
-            if (!isWorkspaceVisible) {
-                workspace.setScaleX(0.92f);
-                workspace.setScaleY(0.92f);
-            }
-            Hotseat hotseat = launcher.getHotseat();
-            boolean isHotseatVisible = hotseat.getVisibility() == VISIBLE && hotseat.getAlpha() > 0;
-            if (!isHotseatVisible) {
-                hotseat.setScaleX(0.92f);
-                hotseat.setScaleY(0.92f);
-            }
-        } else if (this == NORMAL && fromState == OVERVIEW_PEEK) {
-            // Keep fully visible until the very end (when overview is offscreen) to make invisible.
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, t -> t < 1 ? 0 : 1);
-        }
-    }
-
-    protected static void dispatchWindowStateChanged(Launcher launcher) {
-        launcher.getWindow().getDecorView().sendAccessibilityEvent(TYPE_WINDOW_STATE_CHANGED);
-    }
-
     public static abstract class PageAlphaProvider {
 
         public final Interpolator interpolator;
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
deleted file mode 100644
index f673508..0000000
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ /dev/null
@@ -1,674 +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;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.IntDef;
-
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.uioverrides.UiFactory;
-
-import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-
-/**
- * TODO: figure out what kind of tests we can write for this
- *
- * Things to test when changing the following class.
- *   - Home from workspace
- *          - from center screen
- *          - from other screens
- *   - Home from all apps
- *          - from center screen
- *          - from other screens
- *   - Back from all apps
- *          - from center screen
- *          - from other screens
- *   - Launch app from workspace and quit
- *          - with back
- *          - with home
- *   - Launch app from all apps and quit
- *          - with back
- *          - with home
- *   - Go to a screen that's not the default, then all
- *     apps, and launch and app, and go back
- *          - with back
- *          -with home
- *   - On workspace, long press power and go back
- *          - with back
- *          - with home
- *   - On all apps, long press power and go back
- *          - with back
- *          - with home
- *   - On workspace, power off
- *   - On all apps, power off
- *   - Launch an app and turn off the screen while in that app
- *          - Go back with home key
- *          - Go back with back key  TODO: make this not go to workspace
- *          - From all apps
- *          - From workspace
- *   - Enter and exit car mode (becase it causes an extra configuration changed)
- *          - From all apps
- *          - From the center workspace
- *          - From another workspace
- */
-public class LauncherStateManager {
-
-    public static final String TAG = "StateManager";
-
-    // We separate the state animations into "atomic" and "non-atomic" components. The atomic
-    // components may be run atomically - that is, all at once, instead of user-controlled. However,
-    // atomic components are not restricted to this purpose; they can be user-controlled alongside
-    // non atomic components as well. Note that each gesture model has exactly one atomic component,
-    // ATOMIC_OVERVIEW_SCALE_COMPONENT *or* ATOMIC_OVERVIEW_PEEK_COMPONENT.
-    @IntDef(flag = true, value = {
-            NON_ATOMIC_COMPONENT,
-            ATOMIC_OVERVIEW_SCALE_COMPONENT,
-            ATOMIC_OVERVIEW_PEEK_COMPONENT,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface AnimationComponents {}
-    public static final int NON_ATOMIC_COMPONENT = 1 << 0;
-    public static final int ATOMIC_OVERVIEW_SCALE_COMPONENT = 1 << 1;
-    public static final int ATOMIC_OVERVIEW_PEEK_COMPONENT = 1 << 2;
-
-    public static final int ANIM_ALL = NON_ATOMIC_COMPONENT | ATOMIC_OVERVIEW_SCALE_COMPONENT
-            | ATOMIC_OVERVIEW_PEEK_COMPONENT;
-
-    private final AnimationConfig mConfig = new AnimationConfig();
-    private final Handler mUiHandler;
-    private final Launcher mLauncher;
-    private final ArrayList<StateListener> mListeners = new ArrayList<>();
-
-    // Animators which are run on properties also controlled by state animations.
-    private Animator[] mStateElementAnimators;
-
-    private StateHandler[] mStateHandlers;
-    private LauncherState mState = NORMAL;
-
-    private LauncherState mLastStableState = NORMAL;
-    private LauncherState mCurrentStableState = NORMAL;
-
-    private LauncherState mRestState;
-
-    public LauncherStateManager(Launcher l) {
-        mUiHandler = new Handler(Looper.getMainLooper());
-        mLauncher = l;
-    }
-
-    public LauncherState getState() {
-        return mState;
-    }
-
-    public LauncherState getCurrentStableState() {
-        return mCurrentStableState;
-    }
-
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "LauncherState:");
-        writer.println(prefix + "\tmLastStableState:" + mLastStableState);
-        writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
-        writer.println(prefix + "\tmState:" + mState);
-        writer.println(prefix + "\tmRestState:" + mRestState);
-        writer.println(prefix + "\tisInTransition:" + (mConfig.mCurrentAnimation != null));
-    }
-
-    public StateHandler[] getStateHandlers() {
-        if (mStateHandlers == null) {
-            mStateHandlers = UiFactory.getStateHandler(mLauncher);
-        }
-        return mStateHandlers;
-    }
-
-    public void addStateListener(StateListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void removeStateListener(StateListener listener) {
-        mListeners.remove(listener);
-    }
-
-    /**
-     * Returns true if the state changes should be animated.
-     */
-    public boolean shouldAnimateStateChange() {
-        return !mLauncher.isForceInvisible() && mLauncher.isStarted();
-    }
-
-    /**
-     * @see #goToState(LauncherState, boolean, Runnable)
-     */
-    public void goToState(LauncherState state) {
-        goToState(state, shouldAnimateStateChange());
-    }
-
-    /**
-     * @see #goToState(LauncherState, boolean, Runnable)
-     */
-    public void goToState(LauncherState state, boolean animated) {
-        goToState(state, animated, 0, null);
-    }
-
-    /**
-     * Changes the Launcher state to the provided state.
-     *
-     * @param animated false if the state should change immediately without any animation,
-     *                true otherwise
-     * @paras onCompleteRunnable any action to perform at the end of the transition, of null.
-     */
-    public void goToState(LauncherState state, boolean animated, Runnable onCompleteRunnable) {
-        goToState(state, animated, 0, onCompleteRunnable);
-    }
-
-    /**
-     * Changes the Launcher state to the provided state after the given delay.
-     */
-    public void goToState(LauncherState state, long delay, Runnable onCompleteRunnable) {
-        goToState(state, true, delay, onCompleteRunnable);
-    }
-
-    /**
-     * Changes the Launcher state to the provided state after the given delay.
-     */
-    public void goToState(LauncherState state, long delay) {
-        goToState(state, true, delay, null);
-    }
-
-    public void reapplyState() {
-        reapplyState(false);
-    }
-
-    public void reapplyState(boolean cancelCurrentAnimation) {
-        boolean wasInAnimation = mConfig.mCurrentAnimation != null;
-        if (cancelCurrentAnimation) {
-            cancelAllStateElementAnimation();
-            cancelAnimation();
-        }
-        if (mConfig.mCurrentAnimation == null) {
-            for (StateHandler handler : getStateHandlers()) {
-                handler.setState(mState);
-            }
-            if (wasInAnimation) {
-                onStateTransitionEnd(mState);
-            }
-        }
-    }
-
-    private void goToState(LauncherState state, boolean animated, long delay,
-            final Runnable onCompleteRunnable) {
-        animated &= Utilities.areAnimationsEnabled(mLauncher);
-        if (mLauncher.isInState(state)) {
-            if (mConfig.mCurrentAnimation == null) {
-                // Run any queued runnable
-                if (onCompleteRunnable != null) {
-                    onCompleteRunnable.run();
-                }
-                return;
-            } else if (!mConfig.userControlled && animated && mConfig.mTargetState == state) {
-                // We are running the same animation as requested
-                if (onCompleteRunnable != null) {
-                    mConfig.mCurrentAnimation.addListener(new AnimationSuccessListener() {
-                        @Override
-                        public void onAnimationSuccess(Animator animator) {
-                            onCompleteRunnable.run();
-                        }
-                    });
-                }
-                return;
-            }
-        }
-
-        // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
-        LauncherState fromState = mState;
-        mConfig.reset();
-
-        if (!animated) {
-            cancelAllStateElementAnimation();
-            onStateTransitionStart(state);
-            for (StateHandler handler : getStateHandlers()) {
-                handler.setState(state);
-            }
-
-            onStateTransitionEnd(state);
-
-            // Run any queued runnable
-            if (onCompleteRunnable != null) {
-                onCompleteRunnable.run();
-            }
-            return;
-        }
-
-        if (delay > 0) {
-            // Create the animation after the delay as some properties can change between preparing
-            // the animation and running the animation.
-            int startChangeId = mConfig.mChangeId;
-            mUiHandler.postDelayed(() -> {
-                if (mConfig.mChangeId == startChangeId) {
-                    goToStateAnimated(state, fromState, onCompleteRunnable);
-                }
-            }, delay);
-        } else {
-            goToStateAnimated(state, fromState, onCompleteRunnable);
-        }
-    }
-
-    private void goToStateAnimated(LauncherState state, LauncherState fromState,
-            Runnable onCompleteRunnable) {
-        // Since state NORMAL can be reached from multiple states, just assume that the
-        // transition plays in reverse and use the same duration as previous state.
-        mConfig.duration = state == NORMAL ? fromState.transitionDuration : state.transitionDuration;
-
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
-        prepareForAtomicAnimation(fromState, state, builder);
-        AnimatorSet animation = createAnimationToNewWorkspaceInternal(
-                state, builder, onCompleteRunnable);
-        mUiHandler.post(new StartAnimRunnable(animation));
-    }
-
-    /**
-     * Prepares for a non-user controlled animation from fromState to toState. Preparations include:
-     * - Setting interpolators for various animations included in the state transition.
-     * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
-     */
-    public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
-            AnimatorSetBuilder builder) {
-        toState.prepareForAtomicAnimation(mLauncher, fromState, builder);
-    }
-
-    public AnimatorSet createAtomicAnimation(LauncherState fromState, LauncherState toState,
-            AnimatorSetBuilder builder, @AnimationComponents int atomicComponent, long duration) {
-        prepareForAtomicAnimation(fromState, toState, builder);
-        AnimationConfig config = new AnimationConfig();
-        config.animComponents = atomicComponent;
-        config.duration = duration;
-        for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
-            handler.setStateWithAnimation(toState, builder, config);
-        }
-        return builder.build();
-    }
-
-    /**
-     * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
-     * state transition. The UI is force-set to fromState before creating the controller.
-     * @param fromState the initial state for the transition.
-     * @param state the final state for the transition.
-     * @param duration intended duration for normal playback. Use higher duration for better
-     *                accuracy.
-     */
-    public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState fromState, LauncherState state, long duration) {
-        // Since we are creating a state animation to a different state, temporarily prevent state
-        // change as part of config reset.
-        LauncherState originalRestState = mRestState;
-        mRestState = state;
-        mConfig.reset();
-        mRestState = originalRestState;
-
-        for (StateHandler handler : getStateHandlers()) {
-            handler.setState(fromState);
-        }
-
-        return createAnimationToNewWorkspace(state, duration);
-    }
-
-    /**
-     * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
-     * state transition.
-     * @param state the final state for the transition.
-     * @param duration intended duration for normal playback. Use higher duration for better
-     *                accuracy.
-     */
-    public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState state, long duration) {
-        return createAnimationToNewWorkspace(state, duration, LauncherStateManager.ANIM_ALL);
-    }
-
-    public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState state, long duration, @AnimationComponents int animComponents) {
-        return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null,
-                animComponents);
-    }
-
-    public AnimatorPlaybackController createAnimationToNewWorkspace(LauncherState state,
-            AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable,
-            @AnimationComponents int animComponents) {
-        mConfig.reset();
-        mConfig.userControlled = true;
-        mConfig.animComponents = animComponents;
-        mConfig.duration = duration;
-        mConfig.playbackController = AnimatorPlaybackController.wrap(
-                createAnimationToNewWorkspaceInternal(state, builder, null), duration,
-                onCancelRunnable);
-        return mConfig.playbackController;
-    }
-
-    protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state,
-            AnimatorSetBuilder builder, final Runnable onCompleteRunnable) {
-
-        for (StateHandler handler : getStateHandlers()) {
-            handler.setStateWithAnimation(state, builder, mConfig);
-        }
-
-        final AnimatorSet animation = builder.build();
-        animation.addListener(new AnimationSuccessListener() {
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                // Change the internal state only when the transition actually starts
-                onStateTransitionStart(state);
-            }
-
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                // Run any queued runnables
-                if (onCompleteRunnable != null) {
-                    onCompleteRunnable.run();
-                }
-                onStateTransitionEnd(state);
-            }
-        });
-        mConfig.setAnimation(animation, state);
-        return mConfig.mCurrentAnimation;
-    }
-
-    private void onStateTransitionStart(LauncherState state) {
-        if (mState != state) {
-            mState.onStateDisabled(mLauncher);
-        }
-        mState = state;
-        mState.onStateEnabled(mLauncher);
-        mLauncher.onStateSetStart(mState);
-
-        if (state.disablePageClipping) {
-            // Only disable clipping if needed, otherwise leave it as previous value.
-            mLauncher.getWorkspace().setClipChildren(false);
-        }
-        UiFactory.onLauncherStateOrResumeChanged(mLauncher);
-
-        for (int i = mListeners.size() - 1; i >= 0; i--) {
-            mListeners.get(i).onStateTransitionStart(state);
-        }
-    }
-
-    private void onStateTransitionEnd(LauncherState state) {
-        // Only change the stable states after the transitions have finished
-        if (state != mCurrentStableState) {
-            mLastStableState = state.getHistoryForState(mCurrentStableState);
-            mCurrentStableState = state;
-        }
-
-        state.onStateTransitionEnd(mLauncher);
-        mLauncher.onStateSetEnd(state);
-
-        if (state == NORMAL) {
-            setRestState(null);
-        }
-
-        UiFactory.onLauncherStateOrResumeChanged(mLauncher);
-
-        for (int i = mListeners.size() - 1; i >= 0; i--) {
-            mListeners.get(i).onStateTransitionComplete(state);
-        }
-
-        AccessibilityManagerCompat.sendStateEventToTest(mLauncher, state.ordinal);
-    }
-
-    public void onWindowFocusChanged() {
-        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-    }
-
-    public LauncherState getLastState() {
-        return mLastStableState;
-    }
-
-    public void moveToRestState() {
-        if (mConfig.mCurrentAnimation != null && mConfig.userControlled) {
-            // The user is doing something. Lets not mess it up
-            return;
-        }
-        if (mState.disableRestore) {
-            goToState(getRestState());
-            // Reset history
-            mLastStableState = NORMAL;
-        }
-    }
-
-    public LauncherState getRestState() {
-        return mRestState == null ? NORMAL : mRestState;
-    }
-
-    public void setRestState(LauncherState restState) {
-        mRestState = restState;
-    }
-
-    /**
-     * Cancels the current animation.
-     */
-    public void cancelAnimation() {
-        mConfig.reset();
-    }
-
-    public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
-        clearCurrentAnimation();
-        setCurrentAnimation(controller.getTarget());
-        mConfig.userControlled = true;
-        mConfig.playbackController = controller;
-    }
-
-    /**
-     * Sets the animation as the current state animation, i.e., canceled when
-     * starting another animation and may block some launcher interactions while running.
-     *
-     * @param childAnimations Set of animations with the new target is controlling.
-     */
-    public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
-        for (Animator childAnim : childAnimations) {
-            if (childAnim == null) {
-                continue;
-            }
-            if (mConfig.playbackController != null
-                    && mConfig.playbackController.getTarget() == childAnim) {
-                clearCurrentAnimation();
-                break;
-            } else if (mConfig.mCurrentAnimation == childAnim) {
-                clearCurrentAnimation();
-                break;
-            }
-        }
-        boolean reapplyNeeded = mConfig.mCurrentAnimation != null;
-        cancelAnimation();
-        if (reapplyNeeded) {
-            reapplyState();
-            // Dispatch on transition end, so that any transient property is cleared.
-            onStateTransitionEnd(mState);
-        }
-        mConfig.setAnimation(anim, null);
-    }
-
-    private void cancelAllStateElementAnimation() {
-        if (mStateElementAnimators == null) {
-            return;
-        }
-
-        for (Animator animator : mStateElementAnimators) {
-            if (animator != null) {
-                animator.cancel();
-            }
-        }
-    }
-
-    /**
-     * Cancels a currently running gesture animation
-     */
-    public void cancelStateElementAnimation(int index) {
-        if (mStateElementAnimators == null) {
-            return;
-        }
-        if (mStateElementAnimators[index] != null) {
-            mStateElementAnimators[index].cancel();
-        }
-    }
-
-    public Animator createStateElementAnimation(int index, float... values) {
-        cancelStateElementAnimation(index);
-        LauncherAppTransitionManager latm = mLauncher.getAppTransitionManager();
-        if (mStateElementAnimators == null) {
-            mStateElementAnimators = new Animator[latm.getStateElementAnimationsCount()];
-        }
-        Animator anim = latm.createStateElementAnimation(index, values);
-        mStateElementAnimators[index] = anim;
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mStateElementAnimators[index] = null;
-            }
-        });
-        return anim;
-    }
-
-    private void clearCurrentAnimation() {
-        if (mConfig.mCurrentAnimation != null) {
-            mConfig.mCurrentAnimation.removeListener(mConfig);
-            mConfig.mCurrentAnimation = null;
-        }
-        mConfig.playbackController = null;
-    }
-
-    private class StartAnimRunnable implements Runnable {
-
-        private final AnimatorSet mAnim;
-
-        public StartAnimRunnable(AnimatorSet anim) {
-            mAnim = anim;
-        }
-
-        @Override
-        public void run() {
-            if (mConfig.mCurrentAnimation != mAnim) {
-                return;
-            }
-            mAnim.start();
-        }
-    }
-
-    public static class AnimationConfig extends AnimatorListenerAdapter {
-        public long duration;
-        public boolean userControlled;
-        public AnimatorPlaybackController playbackController;
-        public @AnimationComponents int animComponents = ANIM_ALL;
-        private PropertySetter mPropertySetter;
-
-        private AnimatorSet mCurrentAnimation;
-        private LauncherState mTargetState;
-        // Id to keep track of config changes, to tie an animation with the corresponding request
-        private int mChangeId = 0;
-
-        /**
-         * Cancels the current animation and resets config variables.
-         */
-        public void reset() {
-            duration = 0;
-            userControlled = false;
-            animComponents = ANIM_ALL;
-            mPropertySetter = null;
-            mTargetState = null;
-
-            if (playbackController != null) {
-                playbackController.getAnimationPlayer().cancel();
-                playbackController.dispatchOnCancel();
-            } else if (mCurrentAnimation != null) {
-                mCurrentAnimation.setDuration(0);
-                mCurrentAnimation.cancel();
-            }
-
-            mCurrentAnimation = null;
-            playbackController = null;
-            mChangeId ++;
-        }
-
-        public PropertySetter getPropertySetter(AnimatorSetBuilder builder) {
-            if (mPropertySetter == null) {
-                mPropertySetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER
-                        : new AnimatedPropertySetter(duration, builder);
-            }
-            return mPropertySetter;
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            if (playbackController != null && playbackController.getTarget() == animation) {
-                playbackController = null;
-            }
-            if (mCurrentAnimation == animation) {
-                mCurrentAnimation = null;
-            }
-        }
-
-        public void setAnimation(AnimatorSet animation, LauncherState targetState) {
-            mCurrentAnimation = animation;
-            mTargetState = targetState;
-            mCurrentAnimation.addListener(this);
-        }
-
-        public boolean playAtomicOverviewScaleComponent() {
-            return (animComponents & ATOMIC_OVERVIEW_SCALE_COMPONENT) != 0;
-        }
-
-        public boolean playAtomicOverviewPeekComponent() {
-            return (animComponents & ATOMIC_OVERVIEW_PEEK_COMPONENT) != 0;
-        }
-
-        public boolean playNonAtomicComponent() {
-            return (animComponents & NON_ATOMIC_COMPONENT) != 0;
-        }
-    }
-
-    public interface StateHandler {
-
-        /**
-         * Updates the UI to {@param state} without any animations
-         */
-        void setState(LauncherState state);
-
-        /**
-         * Sets the UI to {@param state} by animating any changes.
-         */
-        void setStateWithAnimation(LauncherState toState,
-                AnimatorSetBuilder builder, AnimationConfig config);
-    }
-
-    public interface StateListener {
-
-        void onStateTransitionStart(LauncherState toState);
-        void onStateTransitionComplete(LauncherState finalState);
-    }
-}
diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java
index 95ee687..5f6ecb5 100644
--- a/src/com/android/launcher3/MainProcessInitializer.java
+++ b/src/com/android/launcher3/MainProcessInitializer.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.BitmapCreationCheck;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -39,5 +40,9 @@
         FeatureFlags.initialize(context);
         SessionCommitReceiver.applyDefaultUserPrefs(context);
         IconShape.init(context);
+
+        if (BitmapCreationCheck.ENABLED) {
+            BitmapCreationCheck.startTracking(context);
+        }
     }
 }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ff2b400..e29faac 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -20,6 +20,9 @@
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
+import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
+import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY;
+import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO;
 
 import android.animation.LayoutTransition;
 import android.animation.TimeInterpolator;
@@ -46,13 +49,18 @@
 import android.view.animation.Interpolator;
 import android.widget.ScrollView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds;
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 
@@ -63,8 +71,10 @@
 public abstract class PagedView<T extends View & PageIndicator> extends ViewGroup {
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
+    public static final boolean DEBUG_FAILED_QUICKSWITCH = false;
 
-    protected static final int INVALID_PAGE = -1;
+    public static final int ACTION_MOVE_ALLOW_EASY_FLING = MotionEvent.ACTION_MASK - 1;
+    public static final int INVALID_PAGE = -1;
     protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
     public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
@@ -81,16 +91,16 @@
     // The following constants need to be scaled based on density. The scaled versions will be
     // assigned to the corresponding member variables below.
     private static final int FLING_THRESHOLD_VELOCITY = 500;
+    private static final int EASY_FLING_THRESHOLD_VELOCITY = 400;
     private static final int MIN_SNAP_VELOCITY = 1500;
     private static final int MIN_FLING_VELOCITY = 250;
 
-    public static final int INVALID_RESTORE_PAGE = -1001;
-
     private boolean mFreeScroll = false;
 
-    protected int mFlingThresholdVelocity;
-    protected int mMinFlingVelocity;
-    protected int mMinSnapVelocity;
+    protected final int mFlingThresholdVelocity;
+    protected final int mEasyFlingThresholdVelocity;
+    protected final int mMinFlingVelocity;
+    protected final int mMinSnapVelocity;
 
     protected boolean mFirstLayout = true;
 
@@ -99,8 +109,8 @@
 
     @ViewDebug.ExportedProperty(category = "launcher")
     protected int mNextPage = INVALID_PAGE;
-    protected int mMinScrollX;
-    protected int mMaxScrollX;
+    protected int mMaxScroll;
+    protected int mMinScroll;
     protected OverScroller mScroller;
     private Interpolator mDefaultInterpolator;
     private VelocityTracker mVelocityTracker;
@@ -108,14 +118,21 @@
 
     private float mDownMotionX;
     private float mDownMotionY;
-    private float mLastMotionX;
-    private float mLastMotionXRemainder;
-    private float mTotalMotionX;
+    private float mDownMotionPrimary;
+    private float mLastMotion;
+    private float mLastMotionRemainder;
+    private float mTotalMotion;
+    // Used in special cases where the fling checks can be relaxed for an intentional gesture
+    private boolean mAllowEasyFling;
+    protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
 
     protected int[] mPageScrolls;
     private boolean mIsBeingDragged;
 
+    // The amount of movement to begin scrolling
     protected int mTouchSlop;
+    // The amount of movement to begin paging
+    protected int mPageSlop;
     private int mMaximumVelocity;
     protected boolean mAllowOverScroll = true;
 
@@ -124,12 +141,13 @@
     protected int mActivePointerId = INVALID_POINTER;
 
     protected boolean mIsPageInTransition = false;
+    private Runnable mOnPageTransitionEndCallback;
 
-    protected float mSpringOverScrollX;
+    protected float mSpringOverScroll;
 
     protected boolean mWasInOverscroll = false;
 
-    protected int mUnboundedScrollX;
+    protected int mUnboundedScroll;
 
     // Page Indicator
     @Thunk int mPageIndicatorViewId;
@@ -161,23 +179,19 @@
 
         setHapticFeedbackEnabled(false);
         mIsRtl = Utilities.isRtl(getResources());
-        init();
-    }
 
-    /**
-     * Initializes various states for this workspace.
-     */
-    protected void init() {
-        mScroller = new OverScroller(getContext());
+        mScroller = new OverScroller(context);
         setDefaultInterpolator(Interpolators.SCROLL);
         mCurrentPage = 0;
 
-        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
-        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mPageSlop = configuration.getScaledPagingTouchSlop();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
 
         float density = getResources().getDisplayMetrics().density;
         mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * density);
+        mEasyFlingThresholdVelocity = (int) (EASY_FLING_THRESHOLD_VELOCITY * density);
         mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density);
         mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
 
@@ -238,12 +252,12 @@
      */
     protected void updateCurrentPageScroll() {
         // If the current page is invalid, just reset the scroll position to zero
-        int newX = 0;
+        int newPosition = 0;
         if (0 <= mCurrentPage && mCurrentPage < getPageCount()) {
-            newX = getScrollForPage(mCurrentPage);
+            newPosition = getScrollForPage(mCurrentPage);
         }
-        scrollTo(newX, 0);
-        mScroller.startScroll(mScroller.getCurrPos(), newX - mScroller.getCurrPos());
+        mOrientationHandler.set(this, VIEW_SCROLL_TO, newPosition);
+        mOrientationHandler.scrollerStartScroll(mScroller, newPosition);
         forceFinishScroller(true);
     }
 
@@ -280,7 +294,7 @@
         int dir = !mIsRtl ? 1 : - 1;
         int currScroll = getScrollForPage(page);
         int prevScroll;
-        while (currScroll < mMinScrollX) {
+        while (currScroll < mMinScroll) {
             page += dir;
             prevScroll = currScroll;
             currScroll = getScrollForPage(page);
@@ -289,7 +303,7 @@
                 break;
             }
         }
-        while (currScroll > mMaxScrollX) {
+        while (currScroll > mMaxScroll) {
             page -= dir;
             prevScroll = currScroll;
             currScroll = getScrollForPage(page);
@@ -369,47 +383,62 @@
     protected void onPageEndTransition() {
         mWasInOverscroll = false;
         AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
+        AccessibilityManagerCompat.sendCustomAccessibilityEvent(getPageAt(mCurrentPage),
+                AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
+        if (mOnPageTransitionEndCallback != null) {
+            mOnPageTransitionEndCallback.run();
+            mOnPageTransitionEndCallback = null;
+        }
     }
 
-    protected int getUnboundedScrollX() {
-        return mUnboundedScrollX;
+    /**
+     * Sets a callback to run once when the scrolling finishes. If there is currently
+     * no page in transition, then the callback is called immediately.
+     */
+    public void setOnPageTransitionEndCallback(@Nullable Runnable callback) {
+        if (mIsPageInTransition || callback == null) {
+            mOnPageTransitionEndCallback = callback;
+        } else {
+            callback.run();
+        }
+    }
+
+    protected int getUnboundedScroll() {
+        return mUnboundedScroll;
     }
 
     @Override
     public void scrollBy(int x, int y) {
-        scrollTo(getUnboundedScrollX() + x, getScrollY() + y);
+        mOrientationHandler.delegateScrollBy(this, getUnboundedScroll(), x, y);
     }
 
     @Override
     public void scrollTo(int x, int y) {
-        mUnboundedScrollX = x;
+        int primaryScroll = mOrientationHandler.getPrimaryValue(x, y);
+        int secondaryScroll = mOrientationHandler.getSecondaryValue(x, y);
+        mUnboundedScroll = primaryScroll;
 
-        boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < mMinScrollX);
-        boolean isXAfterLastPage = mIsRtl ? (x < mMinScrollX) : (x > mMaxScrollX);
-
-        if (!isXBeforeFirstPage && !isXAfterLastPage) {
-            mSpringOverScrollX = 0;
+        boolean isBeforeFirstPage = mIsRtl ?
+            (primaryScroll > mMaxScroll) : (primaryScroll < mMinScroll);
+        boolean isAfterLastPage = mIsRtl ?
+            (primaryScroll < mMinScroll) : (primaryScroll > mMaxScroll);
+        if (!isBeforeFirstPage && !isAfterLastPage) {
+            mSpringOverScroll = 0;
         }
 
-        if (isXBeforeFirstPage) {
-            super.scrollTo(mIsRtl ? mMaxScrollX : mMinScrollX, y);
+        if (isBeforeFirstPage) {
+            mOrientationHandler.delegateScrollTo(this,
+                secondaryScroll, mIsRtl ? mMaxScroll : mMinScroll);
             if (mAllowOverScroll) {
                 mWasInOverscroll = true;
-                if (mIsRtl) {
-                    overScroll(x - mMaxScrollX);
-                } else {
-                    overScroll(x - mMinScrollX);
-                }
+                overScroll(primaryScroll - (mIsRtl ? mMaxScroll : mMinScroll));
             }
-        } else if (isXAfterLastPage) {
-            super.scrollTo(mIsRtl ? mMinScrollX : mMaxScrollX, y);
+        } else if (isAfterLastPage) {
+            mOrientationHandler.delegateScrollTo(this,
+                secondaryScroll, mIsRtl ? mMinScroll : mMaxScroll);
             if (mAllowOverScroll) {
                 mWasInOverscroll = true;
-                if (mIsRtl) {
-                    overScroll(x - mMinScrollX);
-                } else {
-                    overScroll(x - mMaxScrollX);
-                }
+                overScroll(primaryScroll - (mIsRtl ? mMinScroll : mMaxScroll));
             }
         } else {
             if (mWasInOverscroll) {
@@ -418,7 +447,13 @@
             }
             super.scrollTo(x, y);
         }
+    }
 
+    /**
+     * Helper for {@link PagedOrientationHandler} to be able to call parent's scrollTo method
+     */
+    public void superScrollTo(int x, int y) {
+        super.scrollTo(x, y);
     }
 
     private void sendScrollAccessibilityEvent() {
@@ -429,9 +464,7 @@
                 ev.setScrollable(true);
                 ev.setScrollX(getScrollX());
                 ev.setScrollY(getScrollY());
-                ev.setMaxScrollX(mMaxScrollX);
-                ev.setMaxScrollY(0);
-
+                mOrientationHandler.setMaxScroll(ev, mMaxScroll);
                 sendAccessibilityEventUnchecked(ev);
             }
         }
@@ -452,9 +485,10 @@
     protected boolean computeScrollHelper(boolean shouldInvalidate) {
         if (mScroller.computeScrollOffset()) {
             // Don't bother scrolling if the page does not need to be moved
-            if (getUnboundedScrollX() != mScroller.getCurrPos()
-                    || getScrollX() != mScroller.getCurrPos()) {
-                scrollTo(mScroller.getCurrPos(), 0);
+            int currentScroll = mOrientationHandler.getPrimaryScroll(this);
+            if (mUnboundedScroll != mScroller.getCurrPos()
+                || currentScroll != mScroller.getCurrPos()) {
+                mOrientationHandler.set(this, VIEW_SCROLL_TO, mScroller.getCurrPos());
             }
             if (shouldInvalidate) {
                 invalidate();
@@ -573,7 +607,8 @@
 
         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
 
-        if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) {
+        boolean isScrollChanged = getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC);
+        if (isScrollChanged) {
             pageScrollChanged = true;
         }
 
@@ -614,7 +649,6 @@
     /**
      * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length
      * of {@code outPageScrolls} should be same as the the childCount
-     *
      */
     protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
             ComputePageScrollsLogic scrollLogic) {
@@ -624,36 +658,30 @@
         final int endIndex = mIsRtl ? -1 : childCount;
         final int delta = mIsRtl ? -1 : 1;
 
-        final int verticalCenter = (getPaddingTop() + getMeasuredHeight() + mInsets.top
-                - mInsets.bottom - getPaddingBottom()) / 2;
+        final int pageCenter = mOrientationHandler.getCenterForPage(this, mInsets);
 
-        final int scrollOffsetLeft = mInsets.left + getPaddingLeft();
-        final int scrollOffsetRight = getWidth() - getPaddingRight() - mInsets.right;
+        final int scrollOffsetStart = mOrientationHandler.getScrollOffsetStart(this, mInsets);
+        final int scrollOffsetEnd = mOrientationHandler.getScrollOffsetEnd(this, mInsets);
         boolean pageScrollChanged = false;
 
-        for (int i = startIndex, childLeft = scrollOffsetLeft; i != endIndex; i += delta) {
+        for (int i = startIndex, childStart = scrollOffsetStart; i != endIndex; i += delta) {
             final View child = getPageAt(i);
             if (scrollLogic.shouldIncludeView(child)) {
-                final int childWidth = child.getMeasuredWidth();
-                final int childRight = childLeft + childWidth;
-
-                if (layoutChildren) {
-                    final int childHeight = child.getMeasuredHeight();
-                    final int childTop = verticalCenter - childHeight / 2;
-                    child.layout(childLeft, childTop, childRight, childTop + childHeight);
-                }
+                ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart,
+                    pageCenter, layoutChildren);
+                final int primaryDimension = bounds.primaryDimension;
+                final int childPrimaryEnd = bounds.childPrimaryEnd;
 
                 // In case the pages are of different width, align the page to left or right edge
                 // based on the orientation.
                 final int pageScroll = mIsRtl
-                        ? (childLeft - scrollOffsetLeft)
-                        : Math.max(0, childRight  - scrollOffsetRight);
+                    ? (childStart - scrollOffsetStart)
+                    : Math.max(0, childPrimaryEnd  - scrollOffsetEnd);
                 if (outPageScrolls[i] != pageScroll) {
                     pageScrollChanged = true;
                     outPageScrolls[i] = pageScroll;
                 }
-
-                childLeft += childWidth + mPageSpacing + getChildGap();
+                childStart += primaryDimension + mPageSpacing + getChildGap();
             }
         }
         return pageScrollChanged;
@@ -664,15 +692,15 @@
     }
 
     protected void updateMinAndMaxScrollX() {
-        mMinScrollX = computeMinScrollX();
-        mMaxScrollX = computeMaxScrollX();
+        mMinScroll = computeMinScroll();
+        mMaxScroll = computeMaxScroll();
     }
 
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         return 0;
     }
 
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         int childCount = getChildCount();
         if (childCount > 0) {
             final int index = mIsRtl ? 0 : childCount - 1;
@@ -715,7 +743,8 @@
 
     protected int getChildOffset(int index) {
         if (index < 0 || index > getChildCount() - 1) return 0;
-        return getPageAt(index).getLeft();
+        View pageAtIndex = getPageAt(index);
+        return mOrientationHandler.getChildStart(pageAtIndex);
     }
 
     @Override
@@ -866,13 +895,13 @@
             case MotionEvent.ACTION_MOVE: {
                 /*
                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
-                 * whether the user has moved far enough from his original down touch.
+                 * whether the user has moved far enough from their original down touch.
                  */
                 if (mActivePointerId != INVALID_POINTER) {
                     determineScrollingStart(ev);
                 }
                 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
-                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
+                // event. in that case, treat the first occurrence of a move event as a ACTION_DOWN
                 // i.e. fall through to the next case (don't break)
                 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
                 // while it's small- this was causing a crash before we checked for INVALID_POINTER)
@@ -885,9 +914,10 @@
                 // Remember location of down touch
                 mDownMotionX = x;
                 mDownMotionY = y;
-                mLastMotionX = x;
-                mLastMotionXRemainder = 0;
-                mTotalMotionX = 0;
+                mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
+                mLastMotionRemainder = 0;
+                mTotalMotion = 0;
+                mAllowEasyFling = false;
                 mActivePointerId = ev.getPointerId(0);
 
                 updateIsBeingDraggedOnTouchDown();
@@ -919,7 +949,7 @@
     private void updateIsBeingDraggedOnTouchDown() {
         // mScroller.isFinished should be false when being flinged.
         final int xDist = Math.abs(mScroller.getFinalPos() - mScroller.getCurrPos());
-        final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3);
+        final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3);
 
         if (finishedScrolling) {
             mIsBeingDragged = false;
@@ -949,17 +979,17 @@
         final int pointerIndex = ev.findPointerIndex(mActivePointerId);
         if (pointerIndex == -1) return;
 
-        final float x = ev.getX(pointerIndex);
-        final int xDiff = (int) Math.abs(x - mLastMotionX);
+        final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
+        final int diff = (int) Math.abs(primaryDirection - mLastMotion);
         final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
-        boolean xMoved = xDiff > touchSlop;
+        boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING;
 
-        if (xMoved) {
+        if (moved) {
             // Scroll if the user moved far enough along the X axis
             mIsBeingDragged = true;
-            mTotalMotionX += Math.abs(mLastMotionX - x);
-            mLastMotionX = x;
-            mLastMotionXRemainder = 0;
+            mTotalMotion += Math.abs(mLastMotion - primaryDirection);
+            mLastMotion = primaryDirection;
+            mLastMotionRemainder = 0;
             onScrollInteractionBegin();
             pageBeginTransition();
             // Stop listening for things like pinches.
@@ -1026,10 +1056,9 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        if (mScroller.isSpringing() && mSpringOverScrollX != 0) {
+        if (mScroller.isSpringing() && mSpringOverScroll != 0) {
             int saveCount = canvas.save();
-
-            canvas.translate(-mSpringOverScrollX, 0);
+            mOrientationHandler.set(canvas, CANVAS_TRANSLATE, -mSpringOverScroll);
             super.dispatchDraw(canvas);
 
             canvas.restoreToCount(saveCount);
@@ -1038,27 +1067,41 @@
         }
     }
 
+    /**
+     * Returns the amount of overscroll caused by the spring in {@link OverScroller}.
+     */
+    private int getSpringOverScroll(int amount) {
+        if (mScroller.isSpringing()) {
+            return amount < 0
+                    ? mScroller.getCurrPos() - mMinScroll
+                    : Math.max(0, mScroller.getCurrPos() - mMaxScroll);
+        } else {
+            return 0;
+        }
+    }
+
     protected void dampedOverScroll(int amount) {
-        mSpringOverScrollX = amount;
         if (amount == 0) {
             return;
         }
 
-        int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth());
-        mSpringOverScrollX = overScrollAmount;
+        int size = mOrientationHandler.getMeasuredSize(this);
+        int overScrollAmount = OverScroll.dampedScroll(amount, size);
         if (mScroller.isSpringing()) {
+            mSpringOverScroll = getSpringOverScroll(amount);
             invalidate();
             return;
         }
 
-        int x = Utilities.boundToRange(getScrollX(), mMinScrollX, mMaxScrollX);
-        super.scrollTo(x + overScrollAmount, getScrollY());
+        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+        int boundedScroll = Utilities.boundToRange(primaryScroll, mMinScroll, mMaxScroll);
+        mOrientationHandler.delegateScrollTo(this, boundedScroll + overScrollAmount);
         invalidate();
     }
 
     protected void overScroll(int amount) {
-        mSpringOverScrollX = amount;
         if (mScroller.isSpringing()) {
+            mSpringOverScroll = getSpringOverScroll(amount);
             invalidate();
             return;
         }
@@ -1066,11 +1109,8 @@
         if (amount == 0) return;
 
         if (mFreeScroll && !mScroller.isFinished()) {
-            if (amount < 0) {
-                super.scrollTo(mMinScrollX + amount, getScrollY());
-            } else {
-                super.scrollTo(mMaxScrollX + amount, getScrollY());
-            }
+            int scrollAmount = amount < 0 ? mMinScroll + amount : mMaxScroll + amount;
+            mOrientationHandler.delegateScrollTo(this, scrollAmount);
         } else {
             dampedOverScroll(amount);
         }
@@ -1120,18 +1160,25 @@
             }
 
             // Remember where the motion event started
-            mDownMotionX = mLastMotionX = ev.getX();
+            mDownMotionX = ev.getX();
             mDownMotionY = ev.getY();
-            mLastMotionXRemainder = 0;
-            mTotalMotionX = 0;
+            mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
+            mLastMotionRemainder = 0;
+            mTotalMotion = 0;
+            mAllowEasyFling = false;
             mActivePointerId = ev.getPointerId(0);
-
             if (mIsBeingDragged) {
                 onScrollInteractionBegin();
                 pageBeginTransition();
             }
             break;
 
+        case ACTION_MOVE_ALLOW_EASY_FLING:
+            // Start scrolling immediately
+            determineScrollingStart(ev);
+            mAllowEasyFling = true;
+            break;
+
         case MotionEvent.ACTION_MOVE:
             if (mIsBeingDragged) {
                 // Scroll to follow the motion event
@@ -1139,18 +1186,18 @@
 
                 if (pointerIndex == -1) return true;
 
-                final float x = ev.getX(pointerIndex);
-                final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
-
-                mTotalMotionX += Math.abs(deltaX);
+                float direction = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
+                float delta = mLastMotion + mLastMotionRemainder - direction;
+                mTotalMotion += Math.abs(delta);
 
                 // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
                 // keep the remainder because we are actually testing if we've moved from the last
                 // scrolled position (which is discrete).
-                if (Math.abs(deltaX) >= 1.0f) {
-                    scrollBy((int) deltaX, 0);
-                    mLastMotionX = x;
-                    mLastMotionXRemainder = deltaX - (int) deltaX;
+                if (Math.abs(delta) >= 1.0f) {
+                    mLastMotion = direction;
+                    mLastMotionRemainder = delta - (int) delta;
+
+                    mOrientationHandler.set(this, VIEW_SCROLL_BY, (int) delta);
                 } else {
                     awakenScrollBars();
                 }
@@ -1163,27 +1210,38 @@
             if (mIsBeingDragged) {
                 final int activePointerId = mActivePointerId;
                 final int pointerIndex = ev.findPointerIndex(activePointerId);
-                final float x = ev.getX(pointerIndex);
+                if (pointerIndex == -1) return true;
+
+                final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev,
+                    pointerIndex);
                 final VelocityTracker velocityTracker = mVelocityTracker;
                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-                int velocityX = (int) velocityTracker.getXVelocity(mActivePointerId);
-                final int deltaX = (int) (x - mDownMotionX);
-                final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth();
-                boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
-                        SIGNIFICANT_MOVE_THRESHOLD;
 
-                mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
-                boolean isFling = mTotalMotionX > mTouchSlop && shouldFlingForVelocity(velocityX);
-                boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0;
-                boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0;
+                int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker,
+                    mActivePointerId);
+                int delta = (int) (primaryDirection - mDownMotionPrimary);
+                int pageOrientedSize = mOrientationHandler.getMeasuredSize(getPageAt(mCurrentPage));
+
+                boolean isSignificantMove = Math.abs(delta) > pageOrientedSize *
+                    SIGNIFICANT_MOVE_THRESHOLD;
+
+                mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection);
+                boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop;
+                boolean isFling = passedSlop && shouldFlingForVelocity(velocity);
+                boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0;
+                boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0;
+                if (DEBUG_FAILED_QUICKSWITCH && !isFling && mAllowEasyFling) {
+                    Log.d("Quickswitch", "isFling=false vel=" + velocity
+                            + " threshold=" + mEasyFlingThresholdVelocity);
+                }
 
                 if (!mFreeScroll) {
                     // In the case that the page is moved far to one direction and then is flung
                     // in the opposite direction, we use a threshold to determine whether we should
                     // just return to the starting page, or if we should skip one further.
                     boolean returnToOriginalPage = false;
-                    if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
-                            Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
+                    if (Math.abs(delta) > pageOrientedSize * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
+                            Math.signum(velocity) != Math.signum(delta) && isFling) {
                         returnToOriginalPage = true;
                     }
 
@@ -1192,15 +1250,15 @@
                     // test for a large move if a fling has been registered. That is, a large
                     // move to the left and fling to the right will register as a fling to the right.
 
-                    if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
-                            (isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
+                    if (((isSignificantMove && !isDeltaLeft && !isFling) ||
+                            (isFling && !isVelocityLeft)) && mCurrentPage > 0) {
                         finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
-                        snapToPageWithVelocity(finalPage, velocityX);
-                    } else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
-                            (isFling && isVelocityXLeft)) &&
+                        snapToPageWithVelocity(finalPage, velocity);
+                    } else if (((isSignificantMove && isDeltaLeft && !isFling) ||
+                            (isFling && isVelocityLeft)) &&
                             mCurrentPage < getChildCount() - 1) {
                         finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
-                        snapToPageWithVelocity(finalPage, velocityX);
+                        snapToPageWithVelocity(finalPage, velocity);
                     } else {
                         snapToDestination();
                     }
@@ -1209,38 +1267,40 @@
                         abortScrollerAnimation(true);
                     }
 
-                    int initialScrollX = getScrollX();
+                    int initialScroll = mOrientationHandler.getPrimaryScroll(this);
+                    int maxScroll = mMaxScroll;
+                    int minScroll = mMinScroll;
 
-                    if (((initialScrollX >= mMaxScrollX) && (isVelocityXLeft || !isFling)) ||
-                            ((initialScrollX <= mMinScrollX) && (!isVelocityXLeft || !isFling))) {
-                        mScroller.springBack(getScrollX(), mMinScrollX, mMaxScrollX);
+                    if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
+                        ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
+                        mScroller.springBack(initialScroll, minScroll, maxScroll);
                         mNextPage = getPageNearestToCenterOfScreen();
                     } else {
                         mScroller.setInterpolator(mDefaultInterpolator);
-                        mScroller.fling(initialScrollX, -velocityX,
-                                mMinScrollX, mMaxScrollX,
-                                Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
+                        mScroller.fling(initialScroll, -velocity,
+                            minScroll, maxScroll,
+                            Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
 
-                        int finalX = mScroller.getFinalPos();
-                        mNextPage = getPageNearestToCenterOfScreen(finalX);
+                        int finalPos = mScroller.getFinalPos();
+                        mNextPage = getPageNearestToCenterOfScreen(finalPos);
 
                         int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
                         int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
-                        if (finalX > mMinScrollX && finalX < mMaxScrollX) {
+                        if (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
                             // will get stopped at the end anyway.
-                            int pageSnappedX = finalX < (firstPageScroll + mMinScrollX) / 2
-                                    ? mMinScrollX
-                                    : finalX > (lastPageScroll + mMaxScrollX) / 2
-                                            ? mMaxScrollX
-                                            : getScrollForPage(mNextPage);
+                            int pageSnapped = finalPos < (firstPageScroll + minScroll) / 2
+                                ? minScroll
+                                : finalPos > (lastPageScroll + maxScroll) / 2
+                                    ? maxScroll
+                                    : getScrollForPage(mNextPage);
 
-                            mScroller.setFinalPos(pageSnappedX);
+                            mScroller.setFinalPos(pageSnapped);
                             // Ensure the scroll/snap doesn't happen too fast;
                             int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
-                                    - mScroller.getDuration();
+                                - mScroller.getDuration();
                             if (extraScrollDuration > 0) {
                                 mScroller.extendDuration(extraScrollDuration);
                             }
@@ -1272,8 +1332,9 @@
         return true;
     }
 
-    protected boolean shouldFlingForVelocity(int velocityX) {
-        return Math.abs(velocityX) > mFlingThresholdVelocity;
+    protected boolean shouldFlingForVelocity(int velocity) {
+        float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
+        return Math.abs(velocity) > threshold;
     }
 
     private void resetTouchState() {
@@ -1296,10 +1357,6 @@
         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_SCROLL: {
-                    Launcher launcher = Launcher.getLauncher(getContext());
-                    if (launcher != null) {
-                        AbstractFloatingView.closeAllOpenViews(launcher);
-                    }
                     // Handle mouse (or ext. device) by shifting the page depending on the scroll
                     final float vscroll;
                     final float hscroll;
@@ -1310,8 +1367,8 @@
                         vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                         hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
                     }
-                    if (Math.abs(vscroll) > Math.abs(hscroll) && !isVerticalScrollable()) {
-                        return true;
+                    if (!canScroll(Math.abs(vscroll), Math.abs(hscroll))) {
+                        return false;
                     }
                     if (hscroll != 0 || vscroll != 0) {
                         boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0)
@@ -1329,8 +1386,13 @@
         return super.onGenericMotionEvent(event);
     }
 
-    protected boolean isVerticalScrollable() {
-        return true;
+    /**
+     * Returns true if the paged view can scroll for the provided vertical and horizontal
+     * scroll values
+     */
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        ActivityContext ac = ActivityContext.lookupContext(getContext());
+        return (ac == null || AbstractFloatingView.getTopOpenView(ac) == null);
     }
 
     private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
@@ -1349,16 +1411,16 @@
     }
 
     private void onSecondaryPointerUp(MotionEvent ev) {
-        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
-                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerIndex = ev.getActionIndex();
         final int pointerId = ev.getPointerId(pointerIndex);
         if (pointerId == mActivePointerId) {
             // This was our active pointer going up. Choose a new
             // active pointer and adjust accordingly.
             // TODO: Make this decision more intelligent.
             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
-            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
-            mLastMotionXRemainder = 0;
+            mLastMotion = mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev,
+                newPointerIndex);
+            mLastMotionRemainder = 0;
             mActivePointerId = ev.getPointerId(newPointerIndex);
             if (mVelocityTracker != null) {
                 mVelocityTracker.clear();
@@ -1376,19 +1438,20 @@
     }
 
     public int getPageNearestToCenterOfScreen() {
-        return getPageNearestToCenterOfScreen(getScrollX());
+        return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
     }
 
-    private int getPageNearestToCenterOfScreen(int scaledScrollX) {
-        int screenCenter = scaledScrollX + (getMeasuredWidth() / 2);
+    private int getPageNearestToCenterOfScreen(int scaledScroll) {
+        int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+        int screenCenter = scaledScroll + (pageOrientationSize / 2);
         int minDistanceFromScreenCenter = Integer.MAX_VALUE;
         int minDistanceFromScreenCenterIndex = -1;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; ++i) {
             View layout = getPageAt(i);
-            int childWidth = layout.getMeasuredWidth();
-            int halfChildWidth = (childWidth / 2);
-            int childCenter = getChildOffset(i) + halfChildWidth;
+            int childSize = mOrientationHandler.getMeasuredSize(layout);
+            int halfChildSize = (childSize / 2);
+            int childCenter = getChildOffset(i) + halfChildSize;
             int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
                 minDistanceFromScreenCenter = distanceFromScreenCenter;
@@ -1403,7 +1466,8 @@
     }
 
     protected boolean isInOverScroll() {
-        return (getScrollX() > mMaxScrollX || getScrollX() < mMinScrollX);
+        int scroll = mOrientationHandler.getPrimaryScroll(this);
+        return scroll > mMaxScroll || scroll < mMinScroll;
     }
 
     protected int getPageSnapDuration() {
@@ -1425,10 +1489,10 @@
 
     protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
         whichPage = validateNewPage(whichPage);
-        int halfScreenSize = getMeasuredWidth() / 2;
+        int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
 
-        final int newX = getScrollForPage(whichPage);
-        int delta = newX - getUnboundedScrollX();
+        final int newLoc = getScrollForPage(whichPage);
+        int delta = newLoc - getUnboundedScroll();
         int duration = 0;
 
         if (Math.abs(velocity) < mMinFlingVelocity) {
@@ -1453,9 +1517,9 @@
         // interpolator at zero, ie. 5. We use 4 to make it a little slower.
         duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
 
-        if (QUICKSTEP_SPRINGS.get()) {
+        if (QUICKSTEP_SPRINGS.get() && mCurrentPage != whichPage) {
             return snapToPage(whichPage, delta, duration, false, null,
-                    velocity * Math.signum(newX - getUnboundedScrollX()), true);
+                    velocity * Math.signum(delta), true);
         } else {
             return snapToPage(whichPage, delta, duration);
         }
@@ -1481,8 +1545,8 @@
             TimeInterpolator interpolator) {
         whichPage = validateNewPage(whichPage);
 
-        int newX = getScrollForPage(whichPage);
-        final int delta = newX - getUnboundedScrollX();
+        int newLoc = getScrollForPage(whichPage);
+        final int delta = newLoc - getUnboundedScroll();
         return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false);
     }
 
@@ -1497,7 +1561,7 @@
             return false;
         }
 
-        if (FeatureFlags.IS_DOGFOOD_BUILD) {
+        if (FeatureFlags.IS_STUDIO_BUILD) {
             duration *= Settings.Global.getFloat(getContext().getContentResolver(),
                     Settings.Global.WINDOW_ANIMATION_SCALE, 1);
         }
@@ -1528,9 +1592,9 @@
         }
 
         if (spring && QUICKSTEP_SPRINGS.get()) {
-            mScroller.startScrollSpring(getUnboundedScrollX(), delta, duration, velocity);
+            mScroller.startScrollSpring(getUnboundedScroll(), delta, duration, velocity);
         } else {
-            mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+            mScroller.startScroll(getUnboundedScroll(), delta, duration);
         }
 
         updatePageIndicator();
diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java
index af5402a..d79f62d 100644
--- a/src/com/android/launcher3/Partner.java
+++ b/src/com/android/launcher3/Partner.java
@@ -53,21 +53,12 @@
     public static final String RES_GRID_NUM_COLUMNS = "grid_num_columns";
     public static final String RES_GRID_ICON_SIZE_DP = "grid_icon_size_dp";
 
-    private static boolean sSearched = false;
-    private static Partner sPartner;
-
     /**
      * Find and return partner details, or {@code null} if none exists.
      */
     public static synchronized Partner get(PackageManager pm) {
-        if (!sSearched) {
-            Pair<String, Resources> apkInfo = findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm);
-            if (apkInfo != null) {
-                sPartner = new Partner(apkInfo.first, apkInfo.second);
-            }
-            sSearched = true;
-        }
-        return sPartner;
+        Pair<String, Resources> apkInfo = findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm);
+        return apkInfo != null ? new Partner(apkInfo.first, apkInfo.second) : null;
     }
 
     private final String mPackageName;
diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java
index 76de3e7..be994ee 100644
--- a/src/com/android/launcher3/PendingAddItemInfo.java
+++ b/src/com/android/launcher3/PendingAddItemInfo.java
@@ -18,10 +18,15 @@
 
 import android.content.ComponentName;
 
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.Optional;
+
 /**
- * Meta data that is used for deferred binding.
- * e.g., this object is used to pass information on draggable targets when they are dropped onto
- * the workspace from another container.
+ * Meta data that is used for deferred binding. e.g., this object is used to pass information on
+ * draggable targets when they are dropped onto the workspace from another container.
  */
 public class PendingAddItemInfo extends ItemInfo {
 
@@ -34,4 +39,22 @@
     protected String dumpProperties() {
         return super.dumpProperties() + " componentName=" + componentName;
     }
+
+    /**
+     * Returns shallow copy of the object.
+     */
+    @Override
+    public ItemInfo makeShallowCopy() {
+        PendingAddItemInfo itemInfo = new PendingAddItemInfo();
+        itemInfo.copyFrom(this);
+        itemInfo.componentName = this.componentName;
+        return itemInfo;
+    }
+
+    @Nullable
+    @Override
+    public ComponentName getTargetComponent() {
+        return Optional.ofNullable(super.getTargetComponent()).orElse(componentName);
+    }
+
 }
diff --git a/src/com/android/launcher3/Reorderable.java b/src/com/android/launcher3/Reorderable.java
new file mode 100644
index 0000000..047fb01
--- /dev/null
+++ b/src/com/android/launcher3/Reorderable.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import android.graphics.PointF;
+import android.view.View;
+
+public interface Reorderable {
+
+    /**
+     * Set the offset related to reorder hint and bounce animations
+     */
+    void setReorderBounceOffset(float x, float y);
+
+    void getReorderBounceOffset(PointF offset);
+
+    /**
+     * Set the offset related to previewing the new reordered position
+     */
+    void setReorderPreviewOffset(float x, float y);
+
+    void getReorderPreviewOffset(PointF offset);
+
+    /**
+     * Set the scale related to reorder hint and "bounce" animations
+     */
+    void setReorderBounceScale(float scale);
+    float getReorderBounceScale();
+
+    /**
+     * Get the com.android.view related to this object
+     */
+    View getView();
+}
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index 73e705b..403d779 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -21,15 +21,15 @@
 import android.util.TypedValue;
 
 public class ResourceUtils {
+    public static final int DEFAULT_NAVBAR_VALUE = 48;
     public static final String NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE = "navigation_bar_width";
     public static final String NAVBAR_BOTTOM_GESTURE_SIZE = "navigation_bar_gesture_height";
 
-
     public static int getNavbarSize(String resName, Resources res) {
-        return getDimenByName(resName, res, 48);
+        return getDimenByName(resName, res, DEFAULT_NAVBAR_VALUE);
     }
 
-    private static int getDimenByName(String resName, Resources res, int defaultValue) {
+    public static int getDimenByName(String resName, Resources res, int defaultValue) {
         final int frameSize;
         final int frameSizeResID = res.getIdentifier(resName, "dimen", "android");
         if (frameSizeResID != 0) {
@@ -40,6 +40,17 @@
         return frameSize;
     }
 
+    public static boolean getBoolByName(String resName, Resources res, boolean defaultValue) {
+        final boolean val;
+        final int resId = res.getIdentifier(resName, "bool", "android");
+        if (resId != 0) {
+            val = res.getBoolean(resId);
+        } else {
+            val = defaultValue;
+        }
+        return val;
+    }
+
     public static int pxFromDp(float size, DisplayMetrics metrics) {
         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index c8c590d..499b54f 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -3,11 +3,16 @@
 import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_COMPLETED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_NO;
 
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
@@ -16,6 +21,7 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Bundle;
@@ -28,20 +34,28 @@
 import android.widget.Toast;
 
 import com.android.launcher3.Launcher.OnResumeCallback;
-import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Themes;
 
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 
 /**
  * Drop target which provides a secondary option for an item.
  *    For app targets: shows as uninstall
  *    For configurable widgets: shows as setup
+ *    For predicted app icons: don't suggest app
  */
 public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmListener {
 
@@ -49,8 +63,9 @@
 
     private static final long CACHE_EXPIRE_TIMEOUT = 5000;
     private final ArrayMap<UserHandle, Boolean> mUninstallDisabledCache = new ArrayMap<>(1);
-
+    private final StatsLogManager mStatsLogManager;
     private final Alarm mCacheExpireAlarm;
+    private boolean mHadPendingAlarm;
 
     protected int mCurrentAccessibilityAction = -1;
     public SecondaryDropTarget(Context context, AttributeSet attrs) {
@@ -59,9 +74,28 @@
 
     public SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-
         mCacheExpireAlarm = new Alarm();
-        mCacheExpireAlarm.setOnAlarmListener(this);
+        mStatsLogManager = StatsLogManager.newInstance(context);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mHadPendingAlarm) {
+            mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
+            mCacheExpireAlarm.setOnAlarmListener(this);
+            mHadPendingAlarm = false;
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mCacheExpireAlarm.alarmPending()) {
+            mCacheExpireAlarm.cancelAlarm();
+            mCacheExpireAlarm.setOnAlarmListener(null);
+            mHadPendingAlarm = true;
+        }
     }
 
     @Override
@@ -80,7 +114,11 @@
             mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint);
             setDrawable(R.drawable.ic_uninstall_shadow);
             updateText(R.string.uninstall_drop_target_label);
-        } else {
+        } else if (action == DISMISS_PREDICTION) {
+            mHoverColor = Themes.getColorAccent(getContext());
+            setDrawable(R.drawable.ic_block_shadow);
+            updateText(R.string.dismiss_prediction_label);
+        } else if (action == RECONFIGURE) {
             mHoverColor = Themes.getColorAccent(getContext());
             setDrawable(R.drawable.ic_setup_shadow);
             updateText(R.string.gadget_setup_text);
@@ -100,8 +138,13 @@
     @Override
     public Target getDropTargetForLogging() {
         Target t = LoggerUtils.newTarget(Target.Type.CONTROL);
-        t.controlType = mCurrentAccessibilityAction == UNINSTALL ? ControlType.UNINSTALL_TARGET
-                : ControlType.SETTINGS_BUTTON;
+        if (mCurrentAccessibilityAction == UNINSTALL) {
+            t.controlType = ControlType.UNINSTALL_TARGET;
+        } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
+            t.controlType = ControlType.DISMISS_PREDICTION;
+        } else {
+            t.controlType = ControlType.SETTINGS_BUTTON;
+        }
         return t;
     }
 
@@ -118,6 +161,9 @@
                 return true;
             }
             return false;
+        } else if (FeatureFlags.ENABLE_PREDICTION_DISMISS.get() && info.isPredictedItem()) {
+            setupUi(DISMISS_PREDICTION);
+            return true;
         }
 
         setupUi(UNINSTALL);
@@ -132,6 +178,7 @@
         }
         // Cancel any pending alarm and set cache expiry after some time
         mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
+        mCacheExpireAlarm.setOnAlarmListener(this);
         if (uninstallDisabled) {
             return false;
         }
@@ -157,7 +204,7 @@
             user = item.user;
         }
         if (intent != null) {
-            LauncherActivityInfo info = LauncherAppsCompat.getInstance(mLauncher)
+            LauncherActivityInfo info = mLauncher.getSystemService(LauncherApps.class)
                     .resolveActivity(intent, user);
             if (info != null
                     && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
@@ -172,6 +219,13 @@
         // Defer onComplete
         d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
         super.onDrop(d, options);
+        if (mCurrentAccessibilityAction == UNINSTALL) {
+            mStatsLogManager.logger().withInstanceId(d.logInstanceId)
+                    .log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
+        } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
+            mStatsLogManager.logger().withInstanceId(d.logInstanceId)
+                    .log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST);
+        }
     }
 
     @Override
@@ -228,6 +282,11 @@
             }
             return null;
         }
+        if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
+            AppLaunchTracker.INSTANCE.get(getContext()).onDismissApp(info.getTargetComponent(),
+                    info.user, AppLaunchTracker.CONTAINER_PREDICTIONS);
+            return null;
+        }
         // else: mCurrentAccessibilityAction == UNINSTALL
 
         ComponentName cn = getUninstallTarget(info);
@@ -279,21 +338,24 @@
         }
 
         @Override
-        public void fillInLogContainerData(View v, ItemInfo info, Target target,
-                Target targetParent) {
-            mOriginal.fillInLogContainerData(v, info, target, targetParent);
+        public void fillInLogContainerData(ItemInfo childInfo, Target child,
+                ArrayList<Target> parents) {
+            mOriginal.fillInLogContainerData(childInfo, child, parents);
         }
 
         @Override
         public void onLauncherResume() {
             // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
-            if (LauncherAppsCompat.getInstance(mContext)
-                    .getApplicationInfo(mPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES,
-                            mDragObject.dragInfo.user) == null) {
+            if (new PackageManagerHelper(mContext).getApplicationInfo(mPackageName,
+                    mDragObject.dragInfo.user, PackageManager.MATCH_UNINSTALLED_PACKAGES) == null) {
                 mDragObject.dragSource = mOriginal;
                 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
+                mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
+                        .log(LAUNCHER_ITEM_UNINSTALL_COMPLETED);
             } else {
                 sendFailure();
+                mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
+                        .log(LAUNCHER_ITEM_UNINSTALL_CANCELLED);
             }
         }
 
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index a87c446..89f0a3d 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
+
 import android.annotation.TargetApi;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -23,6 +25,7 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
@@ -37,14 +40,11 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.compat.PackageInstallerCompat;
 
 import java.util.List;
 
-import static com.android.launcher3.compat.PackageInstallerCompat.getUserHandle;
-
 /**
  * BroadcastReceiver to handle session commit intent.
  */
@@ -77,7 +77,8 @@
             return;
         }
 
-        PackageInstallerCompat packageInstallerCompat = PackageInstallerCompat.getInstance(context);
+        InstallSessionHelper packageInstallerCompat = InstallSessionHelper.INSTANCE.get(context);
+        packageInstallerCompat.restoreDbIfApplicable(info);
         if (TextUtils.isEmpty(info.getAppPackageName())
                 || info.getInstallReason() != PackageManager.INSTALL_REASON_USER
                 || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
@@ -90,9 +91,8 @@
 
     public static void queuePromiseAppIconAddition(Context context, SessionInfo sessionInfo) {
         String packageName = sessionInfo.getAppPackageName();
-        List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(context)
-                .getActivityList(packageName, getUserHandle(sessionInfo));
-        if (activities == null || activities.isEmpty()) {
+        if (context.getSystemService(LauncherApps.class)
+                .getActivityList(packageName, getUserHandle(sessionInfo)).isEmpty()) {
             // Ensure application isn't already installed.
             queueAppIconAddition(context, packageName, sessionInfo.getAppLabel(),
                     sessionInfo.getAppIcon(), getUserHandle(sessionInfo));
@@ -100,9 +100,9 @@
     }
 
     public static void queueAppIconAddition(Context context, String packageName, UserHandle user) {
-        List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(context)
+        List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
                 .getActivityList(packageName, user);
-        if (activities == null || activities.isEmpty()) {
+        if (activities.isEmpty()) {
             // no activity found
             return;
         }
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 1bd8263..6326b7a 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -93,7 +93,7 @@
     public void setupLp(View child) {
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
         if (child instanceof LauncherAppWidgetHostView) {
-            DeviceProfile profile = mActivity.getWallpaperDeviceProfile();
+            DeviceProfile profile = mActivity.getDeviceProfile();
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
                     profile.appWidgetScale.x, profile.appWidgetScale.y);
         } else {
@@ -108,12 +108,12 @@
 
     public int getCellContentHeight() {
         return Math.min(getMeasuredHeight(),
-                mActivity.getWallpaperDeviceProfile().getCellHeight(mContainerType));
+                mActivity.getDeviceProfile().getCellHeight(mContainerType));
     }
 
     public void measureChild(View child) {
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
-        final DeviceProfile profile = mActivity.getWallpaperDeviceProfile();
+        final DeviceProfile profile = mActivity.getDeviceProfile();
 
         if (child instanceof LauncherAppWidgetHostView) {
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
@@ -145,38 +145,46 @@
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
-
-                if (child instanceof LauncherAppWidgetHostView) {
-                    LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
-
-                    // Scale and center the widget to fit within its cells.
-                    DeviceProfile profile = mActivity.getDeviceProfile();
-                    float scaleX = profile.appWidgetScale.x;
-                    float scaleY = profile.appWidgetScale.y;
-
-                    lahv.setScaleToFit(Math.min(scaleX, scaleY));
-                    lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
-                            -(lp.height - (lp.height * scaleY)) / 2.0f);
-                }
-
-                int childLeft = lp.x;
-                int childTop = lp.y;
-                child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
-
-                if (lp.dropped) {
-                    lp.dropped = false;
-
-                    final int[] cellXY = mTmpCellXY;
-                    getLocationOnScreen(cellXY);
-                    mWallpaperManager.sendWallpaperCommand(getWindowToken(),
-                            WallpaperManager.COMMAND_DROP,
-                            cellXY[0] + childLeft + lp.width / 2,
-                            cellXY[1] + childTop + lp.height / 2, 0, null);
-                }
+                layoutChild(child);
             }
         }
     }
 
+    /**
+     * Core logic to layout a child for this ViewGroup.
+     */
+    public void layoutChild(View child) {
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+        if (child instanceof LauncherAppWidgetHostView) {
+            LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
+
+            // Scale and center the widget to fit within its cells.
+            DeviceProfile profile = mActivity.getDeviceProfile();
+            float scaleX = profile.appWidgetScale.x;
+            float scaleY = profile.appWidgetScale.y;
+
+            lahv.setScaleToFit(Math.min(scaleX, scaleY));
+            lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
+                    -(lp.height - (lp.height * scaleY)) / 2.0f);
+        }
+
+        int childLeft = lp.x;
+        int childTop = lp.y;
+        child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
+
+        if (lp.dropped) {
+            lp.dropped = false;
+
+            final int[] cellXY = mTmpCellXY;
+            getLocationOnScreen(cellXY);
+            mWallpaperManager.sendWallpaperCommand(getWindowToken(),
+                    WallpaperManager.COMMAND_DROP,
+                    cellXY[0] + childLeft + lp.width / 2,
+                    cellXY[1] + childTop + lp.height / 2, 0, null);
+        }
+    }
+
+
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == ACTION_DOWN && getAlpha() == 0) {
diff --git a/src/com/android/launcher3/SimpleOnStylusPressListener.java b/src/com/android/launcher3/SimpleOnStylusPressListener.java
deleted file mode 100644
index 6b97dce..0000000
--- a/src/com/android/launcher3/SimpleOnStylusPressListener.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.android.launcher3;
-
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.android.launcher3.StylusEventHelper.StylusButtonListener;
-
-/**
- * Simple listener that performs a long click on the view after a stylus button press.
- */
-public class SimpleOnStylusPressListener implements StylusButtonListener {
-    private View mView;
-
-    public SimpleOnStylusPressListener(View view) {
-        mView = view;
-    }
-
-    public boolean onPressed(MotionEvent event) {
-        return mView.isLongClickable() && mView.performLongClick();
-    }
-
-    public boolean onReleased(MotionEvent event) {
-        return false;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/StylusEventHelper.java b/src/com/android/launcher3/StylusEventHelper.java
deleted file mode 100644
index d5fc0fa..0000000
--- a/src/com/android/launcher3/StylusEventHelper.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.android.launcher3;
-
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-/**
- * Helper for identifying when a stylus touches a view while the primary stylus button is pressed.
- * This can occur in {@value MotionEvent#ACTION_DOWN} or {@value MotionEvent#ACTION_MOVE}.
- */
-public class StylusEventHelper {
-
-    /**
-     * Implement this interface to receive callbacks for a stylus button press and release.
-     */
-    public interface StylusButtonListener {
-        /**
-         * Called when the stylus button is pressed.
-         *
-         * @param event The MotionEvent that the button press occurred for.
-         * @return Whether the event was handled.
-         */
-        public boolean onPressed(MotionEvent event);
-
-        /**
-         * Called when the stylus button is released after a button press. This is also called if
-         * the event is canceled or the stylus is lifted off the screen.
-         *
-         * @param event The MotionEvent the button release occurred for.
-         * @return Whether the event was handled.
-         */
-        public boolean onReleased(MotionEvent event);
-    }
-
-    private boolean mIsButtonPressed;
-    private View mView;
-    private StylusButtonListener mListener;
-    private final float mSlop;
-
-    /**
-     * Constructs a helper for listening to stylus button presses and releases. Ensure that {
-     * {@link #onMotionEvent(MotionEvent)} and {@link #onGenericMotionEvent(MotionEvent)} are called on
-     * the helper to correctly identify stylus events.
-     *
-     * @param listener The listener to call for stylus events.
-     * @param view Optional view associated with the touch events.
-     */
-    public StylusEventHelper(StylusButtonListener listener, View view) {
-        mListener = listener;
-        mView = view;
-        if (mView != null) {
-            mSlop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
-        } else {
-            mSlop = ViewConfiguration.getTouchSlop();
-        }
-    }
-
-    public boolean onMotionEvent(MotionEvent event) {
-        final boolean stylusButtonPressed = isStylusButtonPressed(event);
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mIsButtonPressed = stylusButtonPressed;
-                if (mIsButtonPressed) {
-                    return mListener.onPressed(event);
-                }
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (!Utilities.pointInView(mView, event.getX(), event.getY(), mSlop)) {
-                    return false;
-                }
-                if (!mIsButtonPressed && stylusButtonPressed) {
-                    mIsButtonPressed = true;
-                    return mListener.onPressed(event);
-                } else if (mIsButtonPressed && !stylusButtonPressed) {
-                    mIsButtonPressed = false;
-                    return mListener.onReleased(event);
-                }
-                break;
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                if (mIsButtonPressed) {
-                    mIsButtonPressed = false;
-                    return mListener.onReleased(event);
-                }
-                break;
-        }
-        return false;
-    }
-
-    /**
-     * Whether a stylus button press is occurring.
-     */
-    public boolean inStylusButtonPressed() {
-        return mIsButtonPressed;
-    }
-
-    /**
-     * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button
-     * pressed.
-     *
-     * @param event The event to check.
-     * @return Whether a stylus button press occurred.
-     */
-    private static boolean isStylusButtonPressed(MotionEvent event) {
-        return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
-                && ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY)
-                        == MotionEvent.BUTTON_SECONDARY);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 5d0effa..bf63788 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_ICON_BADGED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ICON_BADGED;
 
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
@@ -24,9 +24,13 @@
 import android.app.Person;
 import android.app.WallpaperManager;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.res.Resources;
@@ -59,17 +63,22 @@
 import android.view.ViewConfiguration;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
+import androidx.core.os.BuildCompat;
+
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.graphics.GridOptionsProvider;
 import com.android.launcher3.graphics.TintedDrawableSpan;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.icons.ShortcutCachingLogic;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.views.Transposable;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.lang.reflect.Method;
@@ -97,6 +106,8 @@
     public static final String[] EMPTY_STRING_ARRAY = new String[0];
     public static final Person[] EMPTY_PERSON_ARRAY = new Person[0];
 
+    public static final boolean ATLEAST_R = BuildCompat.isAtLeastR();
+
     public static final boolean ATLEAST_Q = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
 
     public static final boolean ATLEAST_P =
@@ -163,7 +174,7 @@
     public static float getDescendantCoordRelativeToAncestor(
             View descendant, View ancestor, float[] coord, boolean includeRootScroll) {
         return getDescendantCoordRelativeToAncestor(descendant, ancestor, coord, includeRootScroll,
-                false, null);
+                false);
     }
 
     /**
@@ -176,15 +187,12 @@
      * @param includeRootScroll Whether or not to account for the scroll of the descendant:
      *          sometimes this is relevant as in a child's coordinates within the descendant.
      * @param ignoreTransform If true, view transform is ignored
-     * @param outRotation If not null, and {@param ignoreTransform} is true, this is set to the
-     *                   overall rotation of the view in degrees.
      * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
      *         this scale factor is assumed to be equal in X and Y, and so if at any point this
      *         assumption fails, we will need to return a pair of scale factors.
      */
     public static float getDescendantCoordRelativeToAncestor(View descendant, View ancestor,
-            float[] coord, boolean includeRootScroll, boolean ignoreTransform,
-            float[] outRotation) {
+            float[] coord, boolean includeRootScroll, boolean ignoreTransform) {
         float scale = 1.0f;
         View v = descendant;
         while(v != ancestor && v != null) {
@@ -194,19 +202,7 @@
                 offsetPoints(coord, -v.getScrollX(), -v.getScrollY());
             }
 
-            if (ignoreTransform) {
-                if (v instanceof Transposable) {
-                    RotationMode m = ((Transposable) v).getRotationMode();
-                    if (m.isTransposed) {
-                        sMatrix.setRotate(m.surfaceRotation, v.getPivotX(), v.getPivotY());
-                        sMatrix.mapPoints(coord);
-
-                        if (outRotation != null) {
-                            outRotation[0] += m.surfaceRotation;
-                        }
-                    }
-                }
-            } else {
+            if (!ignoreTransform) {
                 v.getMatrix().mapPoints(coord);
             }
             offsetPoints(coord, v.getLeft(), v.getTop());
@@ -353,6 +349,30 @@
     }
 
     /**
+     * Bounds parameter to the range [0, 1]
+     */
+    public static float saturate(float a) {
+        return boundToRange(a, 0, 1.0f);
+    }
+
+    /**
+     * Returns the compliment (1 - a) of the parameter.
+     */
+    public static float comp(float a) {
+        return 1 - a;
+    }
+
+    /**
+     * Returns the "probabilistic or" of a and b. (a + b - ab).
+     * Useful beyond probability, can be used to combine two unit progresses for example.
+     */
+    public static float or(float a, float b) {
+        float satA = saturate(a);
+        float satB = saturate(b);
+        return satA + satB - (satA * satB);
+    }
+
+    /**
      * Trims the string, removing all whitespace at the beginning and end of the string.
      * Non-breaking whitespaces are also removed.
      */
@@ -380,7 +400,7 @@
         return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
     }
 
-    public static float dpiFromPx(int size, DisplayMetrics metrics){
+    public static float dpiFromPx(float size, DisplayMetrics metrics) {
         float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
         return (size / densityRatio);
     }
@@ -463,15 +483,26 @@
     }
 
     public static SharedPreferences getPrefs(Context context) {
-        return context.getSharedPreferences(
+        // Use application context for shared preferences, so that we use a single cached instance
+        return context.getApplicationContext().getSharedPreferences(
                 LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
     }
 
     public static SharedPreferences getDevicePrefs(Context context) {
-        return context.getSharedPreferences(
+        // Use application context for shared preferences, so that we use a single cached instance
+        return context.getApplicationContext().getSharedPreferences(
                 LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
     }
 
+    /**
+     * @return {@link SharedPreferences} that backs {@link FeatureFlags}
+     */
+    public static SharedPreferences getFeatureFlagsPrefs(Context context) {
+        // Use application context for shared preferences, so that we use a single cached instance
+        return context.getApplicationContext().getSharedPreferences(
+            FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+    }
+
     public static boolean areAnimationsEnabled(Context context) {
         return ATLEAST_OREO
                 ? ValueAnimator.areAnimatorsEnabled()
@@ -487,6 +518,42 @@
                 || e.getCause() instanceof DeadObjectException;
     }
 
+    public static boolean isGridOptionsEnabled(Context context) {
+        return isComponentEnabled(context.getPackageManager(),
+                context.getPackageName(),
+                GridOptionsProvider.class.getName());
+    }
+
+    private static boolean isComponentEnabled(PackageManager pm, String pkgName, String clsName) {
+        ComponentName componentName = new ComponentName(pkgName, clsName);
+        int componentEnabledSetting = pm.getComponentEnabledSetting(componentName);
+
+        switch (componentEnabledSetting) {
+            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
+                return false;
+            case PackageManager.COMPONENT_ENABLED_STATE_ENABLED:
+                return true;
+            case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
+            default:
+                // We need to get the application info to get the component's default state
+                try {
+                    PackageInfo packageInfo = pm.getPackageInfo(pkgName,
+                            PackageManager.GET_PROVIDERS | PackageManager.GET_DISABLED_COMPONENTS);
+
+                    if (packageInfo.providers != null) {
+                        return Arrays.stream(packageInfo.providers).anyMatch(
+                                pi -> pi.name.equals(clsName) && pi.isEnabled());
+                    }
+
+                    // the component is not declared in the AndroidManifest
+                    return false;
+                } catch (PackageManager.NameNotFoundException e) {
+                    // the package isn't installed on the device
+                    return false;
+                }
+        }
+    }
+
     /**
      * Utility method to post a runnable on the handler, skipping the synchronization barriers.
      */
@@ -518,19 +585,20 @@
     }
 
     /**
-     * Returns the full drawable for {@param info}.
+     * Returns the full drawable for info without any flattening or pre-processing.
+     *
      * @param outObj this is set to the internal data associated with {@param info},
      *               eg {@link LauncherActivityInfo} or {@link ShortcutInfo}.
      */
     public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
-            boolean flattenDrawable, Object[] outObj) {
+            Object[] outObj) {
         LauncherAppState appState = LauncherAppState.getInstance(launcher);
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-            LauncherActivityInfo activityInfo = LauncherAppsCompat.getInstance(launcher)
+            LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class)
                     .resolveActivity(info.getIntent(), info.user);
             outObj[0] = activityInfo;
-            return (activityInfo != null) ? appState.getIconCache()
-                    .getFullResIcon(activityInfo, flattenDrawable) : null;
+            return activityInfo == null ? null : new IconProvider(launcher).getIconForUI(
+                    activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             if (info instanceof PendingAddShortcutInfo) {
                 ShortcutConfigActivityInfo activityInfo =
@@ -538,15 +606,15 @@
                 outObj[0] = activityInfo;
                 return activityInfo.getFullResIcon(appState.getIconCache());
             }
-            ShortcutKey key = ShortcutKey.fromItemInfo(info);
-            DeepShortcutManager sm = DeepShortcutManager.getInstance(launcher);
-            List<ShortcutInfo> si = sm.queryForFullDetails(
-                    key.componentName.getPackageName(), Arrays.asList(key.getId()), key.user);
+            if (info.getIntent() == null || info.getIntent().getPackage() == null) return null;
+            List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
+                    .buildRequest(launcher)
+                    .query(ShortcutRequest.ALL);
             if (si.isEmpty()) {
                 return null;
             } else {
                 outObj[0] = si.get(0);
-                return sm.getShortcutIconDrawable(si.get(0),
+                return ShortcutCachingLogic.getIcon(launcher, si.get(0),
                         appState.getInvariantDeviceProfile().fillResIconDpi);
             }
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
@@ -581,9 +649,8 @@
                 return new FixedSizeEmptyDrawable(iconSize);
             }
             ShortcutInfo si = (ShortcutInfo) obj;
-            LauncherIcons li = LauncherIcons.obtain(appState.getContext());
-            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).iconBitmap;
-            li.recycle();
+            Bitmap badge = LauncherAppState.getInstance(appState.getContext())
+                    .getIconCache().getShortcutInfoBadge(si).icon;
             float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize);
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index c6381b0..2c45c77 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -23,24 +23,27 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.CancellationSignal;
 import android.os.Process;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.Pair;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.ShadowGenerator;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.PackageUserKey;
@@ -48,6 +51,7 @@
 import com.android.launcher3.util.SQLiteCacheHelper;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -74,17 +78,65 @@
 
     private final Context mContext;
     private final IconCache mIconCache;
-    private final UserManagerCompat mUserManager;
+    private final UserCache mUserCache;
     private final CacheDb mDb;
 
+    private final UserHandle mMyUser = Process.myUserHandle();
+    private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
+
     public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
         mIconCache = iconCache;
-        mUserManager = UserManagerCompat.getInstance(context);
+        mUserCache = UserCache.INSTANCE.get(context);
         mDb = new CacheDb(context);
     }
 
     /**
+     * Returns a drawable that can be used as a badge for the user or null.
+     */
+    @UiThread
+    public Drawable getBadgeForUser(UserHandle user, int badgeSize) {
+        if (mMyUser.equals(user)) {
+            return null;
+        }
+
+        Bitmap badgeBitmap = getUserBadge(user, badgeSize);
+        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
+        d.setFilterBitmap(true);
+        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
+        return d;
+    }
+
+    private Bitmap getUserBadge(UserHandle user, int badgeSize) {
+        synchronized (mUserBadges) {
+            Bitmap badgeBitmap = mUserBadges.get(user);
+            if (badgeBitmap != null) {
+                return badgeBitmap;
+            }
+
+            final Resources res = mContext.getResources();
+            badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
+
+            Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity(
+                    new BitmapDrawable(res, badgeBitmap), user,
+                    new Rect(0, 0, badgeSize, badgeSize),
+                    0);
+            if (drawable instanceof BitmapDrawable) {
+                badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
+            } else {
+                badgeBitmap.eraseColor(Color.TRANSPARENT);
+                Canvas c = new Canvas(badgeBitmap);
+                drawable.setBounds(0, 0, badgeSize, badgeSize);
+                drawable.draw(c);
+                c.setBitmap(null);
+            }
+
+            mUserBadges.put(user, badgeBitmap);
+            return badgeBitmap;
+        }
+    }
+
+    /**
      * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
      * called on UI thread
      *
@@ -105,8 +157,8 @@
 
     public void refresh() {
         mDb.clear();
-
     }
+
     /**
      * The DB holds the generated previews for various components. Previews can also have different
      * sizes (landscape vs portrait).
@@ -145,7 +197,7 @@
     @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
         ContentValues values = new ContentValues();
         values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
-        values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user));
+        values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
         values.put(CacheDb.COLUMN_SIZE, key.size);
         values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
         values.put(CacheDb.COLUMN_VERSION, versions[0]);
@@ -155,7 +207,7 @@
     }
 
     public void removePackage(String packageName, UserHandle user) {
-        removePackage(packageName, user, mUserManager.getSerialNumberForUser(user));
+        removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
     }
 
     private void removePackage(String packageName, UserHandle user, long userSerial) {
@@ -184,7 +236,7 @@
         LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
 
         for (ComponentKey key : list) {
-            final long userId = mUserManager.getSerialNumberForUser(key.user);
+            final long userId = mUserCache.getSerialNumberForUser(key.user);
             HashSet<String> packages = validPackages.get(userId);
             if (packages == null) {
                 packages = new HashSet<>();
@@ -195,7 +247,7 @@
 
         LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
         long passedUserId = packageUser == null ? 0
-                : mUserManager.getSerialNumberForUser(packageUser.mUser);
+                : mUserCache.getSerialNumberForUser(packageUser.mUser);
         Cursor c = null;
         try {
             c = mDb.query(
@@ -234,7 +286,7 @@
 
             for (int i = 0; i < packagesToDelete.size(); i++) {
                 long userId = packagesToDelete.keyAt(i);
-                UserHandle user = mUserManager.getUserForSerialNumber(userId);
+                UserHandle user = mUserCache.getUserForSerialNumber(userId);
                 for (String pkg : packagesToDelete.valueAt(i)) {
                     removePackage(pkg, user, userId);
                 }
@@ -260,7 +312,7 @@
                             + CacheDb.COLUMN_SIZE + " = ?",
                     new String[]{
                             key.componentName.flattenToShortString(),
-                            Long.toString(mUserManager.getSerialNumberForUser(key.user)),
+                            Long.toString(mUserCache.getSerialNumberForUser(key.user)),
                             key.size
                     });
             // If cancelled, skip getting the blob and decoding it into a bitmap
@@ -289,19 +341,30 @@
         return null;
     }
 
-    private Bitmap generatePreview(BaseActivity launcher, WidgetItem item, Bitmap recycle,
+    /**
+     * Returns generatedPreview for a widget and if the preview should be saved in persistent
+     * storage.
+     * @param launcher
+     * @param item
+     * @param recycle
+     * @param previewWidth
+     * @param previewHeight
+     * @return Pair<Bitmap, Boolean>
+     */
+    private Pair<Bitmap, Boolean> generatePreview(BaseActivity launcher, WidgetItem item,
+            Bitmap recycle,
             int previewWidth, int previewHeight) {
         if (item.widgetInfo != null) {
             return generateWidgetPreview(launcher, item.widgetInfo,
                     previewWidth, recycle, null);
         } else {
-            return generateShortcutPreview(launcher, item.activityInfo,
-                    previewWidth, previewHeight, recycle);
+            return new Pair<>(generateShortcutPreview(launcher, item.activityInfo,
+                    previewWidth, previewHeight, recycle), false);
         }
     }
 
     /**
-     * Generates the widget preview from either the {@link AppWidgetManagerCompat} or cache
+     * Generates the widget preview from either the {@link WidgetManagerHelper} or cache
      * and add badge at the bottom right corner.
      *
      * @param launcher
@@ -309,9 +372,10 @@
      * @param maxPreviewWidth             width of the preview on either workspace or tray
      * @param preview                     bitmap that can be recycled
      * @param preScaledWidthOut           return the width of the returned bitmap
-     * @return
+     * @return Pair<Bitmap (the preview) , Boolean (should be stored in db)>
      */
-    public Bitmap generateWidgetPreview(BaseActivity launcher, LauncherAppWidgetProviderInfo info,
+    public Pair<Bitmap, Boolean> generateWidgetPreview(BaseActivity launcher,
+            LauncherAppWidgetProviderInfo info,
             int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
         // Load the preview image if possible
         if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
@@ -341,6 +405,8 @@
         int previewWidth;
         int previewHeight;
 
+        boolean savePreviewImage = widgetPreviewExists || info.previewImage == 0;
+
         if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
                 && drawable.getIntrinsicHeight() > 0) {
             previewWidth = drawable.getIntrinsicWidth();
@@ -427,10 +493,12 @@
                     icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
                     icon.draw(c);
                 }
-            } catch (Resources.NotFoundException e) { }
+            } catch (Resources.NotFoundException e) {
+                savePreviewImage = false;
+            }
             c.setBitmap(null);
         }
-        return preview;
+        return new Pair<>(preview, savePreviewImage);
     }
 
     private RectF drawBoxWithShadow(Canvas c, int width, int height) {
@@ -533,6 +601,8 @@
         @Thunk long[] mVersions;
         @Thunk Bitmap mBitmapToRecycle;
 
+        private boolean mSaveToDB = false;
+
         PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
                 int previewHeight, WidgetCell caller) {
             mKey = key;
@@ -588,7 +658,10 @@
                         : null;
 
                 // it's not in the db... we need to generate it
-                preview = generatePreview(mActivity, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
+                Pair<Bitmap, Boolean> pair = generatePreview(mActivity, mInfo, unusedBitmap,
+                        mPreviewWidth, mPreviewHeight);
+                preview = pair.first;
+                this.mSaveToDB = pair.second;
             }
             return preview;
         }
@@ -602,7 +675,7 @@
                 MODEL_EXECUTOR.post(new Runnable() {
                     @Override
                     public void run() {
-                        if (!isCancelled()) {
+                        if (!isCancelled() && mSaveToDB) {
                             // If we are still using this preview, then write it to the DB and then
                             // let the normal clear mechanism recycle the bitmap
                             writeToDb(mKey, mVersions, preview);
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 9eeb286..1441e0b 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -16,20 +16,25 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
-import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_TRANSITION_MS;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
+import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED;
+import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_INACCESSIBLE;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.LayoutTransition;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.SuppressLint;
@@ -39,12 +44,10 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Message;
 import android.os.Parcelable;
 import android.os.UserHandle;
@@ -60,32 +63,38 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Toast;
 
-import com.android.launcher3.Launcher.LauncherOverlay;
 import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
-import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.FolderDotInfo;
 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.dragndrop.DraggableView;
 import com.android.launcher3.dragndrop.SpringLoadedDragController;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.logging.UserEventDispatcher;
+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.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -101,6 +110,8 @@
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
+import com.android.launcher3.widget.WidgetManagerHelper;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -113,7 +124,7 @@
  */
 public class Workspace extends PagedView<WorkspacePageIndicator>
         implements DropTarget, DragSource, View.OnTouchListener,
-        DragController.DragListener, Insettable, LauncherStateManager.StateHandler,
+        DragController.DragListener, Insettable, StateHandler<LauncherState>,
         WorkspaceLayoutManager {
 
     /** The value that {@link #mTransitionProgress} must be greater than for
@@ -126,12 +137,9 @@
 
     private static final boolean ENFORCE_DRAG_EVENT_ORDER = false;
 
-    private static final int SNAP_OFF_EMPTY_SCREEN_DURATION = 400;
-    private static final int FADE_EMPTY_SCREEN_DURATION = 150;
-
     private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
 
-    private static final int DEFAULT_PAGE = 0;
+    public static final int DEFAULT_PAGE = 0;
 
     private LayoutTransition mLayoutTransition;
     @Thunk final WallpaperManager mWallpaperManager;
@@ -141,7 +149,6 @@
     @Thunk final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
     @Thunk final IntArray mScreenOrder = new IntArray();
 
-    @Thunk Runnable mRemoveEmptyScreenRunnable;
     @Thunk boolean mDeferRemoveExtraEmptyScreen = false;
 
     /**
@@ -193,10 +200,7 @@
     final WallpaperOffsetInterpolator mWallpaperOffset;
     private boolean mUnlockWallpaperFromDefaultPageOnLayout;
 
-    // Variables relating to the creation of user folders by hovering shortcuts over shortcuts
-    private static final int FOLDER_CREATION_TIMEOUT = 0;
     public static final int REORDER_TIMEOUT = 650;
-    private final Alarm mFolderCreationAlarm = new Alarm();
     private final Alarm mReorderAlarm = new Alarm();
     private PreviewBackground mFolderCreateBg;
     private FolderIcon mDragOverFolderIcon = null;
@@ -249,6 +253,8 @@
     // Handles workspace state transitions
     private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
 
+    private final StatsLogManager mStatsLogManager;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -281,25 +287,20 @@
         // Disable multitouch across the workspace/all apps/customize tray
         setMotionEventSplittingEnabled(true);
         setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
+        mStatsLogManager = StatsLogManager.newInstance(context);
     }
 
     @Override
     public void setInsets(Rect insets) {
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        DeviceProfile stableGrid = mLauncher.getWallpaperDeviceProfile();
 
-        mMaxDistanceForFolderCreation = stableGrid.isTablet
-                ? 0.75f * stableGrid.iconSizePx
-                : 0.55f * stableGrid.iconSizePx;
+        mMaxDistanceForFolderCreation = grid.isTablet
+                ? 0.75f * grid.iconSizePx : 0.55f * grid.iconSizePx;
         mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
 
-        Rect padding = stableGrid.workspacePadding;
-
-        RotationMode rotationMode = mLauncher.getRotationMode();
-
-        rotationMode.mapRect(padding, mTempRect);
-        setPadding(mTempRect.left, mTempRect.top, mTempRect.right, mTempRect.bottom);
-        rotationMode.mapRect(stableGrid.getInsets(), mInsets);
+        Rect padding = grid.workspacePadding;
+        setPadding(padding.left, padding.top, padding.right, padding.bottom);
+        mInsets.set(insets);
 
         if (mWorkspaceFadeInAdjacentScreens) {
             // In landscape mode the page spacing is set to the default.
@@ -312,12 +313,11 @@
         }
 
 
-        int paddingLeftRight = stableGrid.cellLayoutPaddingLeftRightPx;
-        int paddingBottom = stableGrid.cellLayoutBottomPaddingPx;
+        int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
+        int paddingBottom = grid.cellLayoutBottomPaddingPx;
         for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) {
-            CellLayout page = mWorkspaceScreens.valueAt(i);
-            page.setRotationMode(rotationMode);
-            page.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
+            mWorkspaceScreens.valueAt(i)
+                    .setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
         }
     }
 
@@ -337,7 +337,7 @@
 
             float scale = 1;
             if (isWidget) {
-                DeviceProfile profile = mLauncher.getWallpaperDeviceProfile();
+                DeviceProfile profile = mLauncher.getDeviceProfile();
                 scale = Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
             }
             size[0] = r.width();
@@ -367,7 +367,7 @@
     }
 
     @Override
-    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+    public void onDragStart(DragObject dragObject, DragOptions options) {
         if (ENFORCE_DRAG_EVENT_ORDER) {
             enforceDragParity("onDragStart", 0, 0);
         }
@@ -418,10 +418,10 @@
         }
 
         // Always enter the spring loaded mode
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Switching to SPRING_LOADED");
-        }
         mLauncher.getStateManager().goToState(SPRING_LOADED);
+        mStatsLogManager.logger().withItemInfo(dragObject.dragInfo)
+                .withInstanceId(dragObject.logInstanceId)
+                .log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
     }
 
     public void deferRemoveExtraEmptyScreen() {
@@ -435,7 +435,7 @@
         }
 
         if (!mDeferRemoveExtraEmptyScreen) {
-            removeExtraEmptyScreen(true, mDragSourceInternal != null);
+            removeExtraEmptyScreen(mDragSourceInternal != null);
         }
 
         updateChildrenLayersEnabled();
@@ -460,8 +460,16 @@
     private void setupLayoutTransition() {
         // We want to show layout transitions when pages are deleted, to close the gap.
         mLayoutTransition = new LayoutTransition();
+
         mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
         mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+        // Change the interpolators such that the fade animation plays before the move animation.
+        // This prevents empty adjacent pages to overlay during animation
+        mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
+                Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0, 0.5f));
+        mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
+                Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0.5f, 1));
+
         mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
         mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
         setLayoutTransition(mLayoutTransition);
@@ -500,7 +508,7 @@
             // In transposed layout, we add the QSB in the Grid. As workspace does not touch the
             // edges, we do not need a full width QSB.
             qsb = LayoutInflater.from(getContext())
-                    .inflate(R.layout.search_container_workspace,firstPage, false);
+                    .inflate(R.layout.search_container_workspace, firstPage, false);
         }
 
         CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
@@ -560,10 +568,9 @@
         // created CellLayout.
         CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
                         R.layout.workspace_screen, this, false /* attachToRoot */);
-        DeviceProfile grid = mLauncher.getWallpaperDeviceProfile();
+        DeviceProfile grid = mLauncher.getDeviceProfile();
         int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
         int paddingBottom = grid.cellLayoutBottomPaddingPx;
-        newScreen.setRotationMode(mLauncher.getRotationMode());
         newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
 
         mWorkspaceScreens.put(screenId, newScreen);
@@ -571,11 +578,6 @@
         addView(newScreen, insertIndex);
         mStateTransitionAnimation.applyChildState(
                 mLauncher.getStateManager().getState(), newScreen, insertIndex);
-
-        if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
-            newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
-        }
-
         return newScreen;
     }
 
@@ -583,9 +585,6 @@
         boolean lastChildOnScreen = false;
         boolean childOnFinalScreen = false;
 
-        // Cancel any pending removal of empty screen
-        mRemoveEmptyScreenRunnable = null;
-
         if (mDragSourceInternal != null) {
             if (mDragSourceInternal.getChildCount() == 1) {
                 lastChildOnScreen = true;
@@ -636,43 +635,34 @@
         }
     }
 
-    public void removeExtraEmptyScreen(final boolean animate, boolean stripEmptyScreens) {
-        removeExtraEmptyScreenDelayed(animate, null, 0, stripEmptyScreens);
+    public void removeExtraEmptyScreen(boolean stripEmptyScreens) {
+        removeExtraEmptyScreenDelayed(0, stripEmptyScreens, null);
     }
 
-    public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete,
-            final int delay, final boolean stripEmptyScreens) {
+    public void removeExtraEmptyScreenDelayed(
+            int delay, boolean stripEmptyScreens, Runnable onComplete) {
         if (mLauncher.isWorkspaceLoading()) {
             // Don't strip empty screens if the workspace is still loading
             return;
         }
 
         if (delay > 0) {
-            postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    removeExtraEmptyScreenDelayed(animate, onComplete, 0, stripEmptyScreens);
-                }
-            }, delay);
+            postDelayed(
+                    () -> removeExtraEmptyScreenDelayed(0, stripEmptyScreens, onComplete), delay);
             return;
         }
 
         convertFinalScreenToEmptyScreenIfNecessary();
         if (hasExtraEmptyScreen()) {
-            int emptyIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
-            if (getNextPage() == emptyIndex) {
-                snapToPage(getNextPage() - 1, SNAP_OFF_EMPTY_SCREEN_DURATION);
-                fadeAndRemoveEmptyScreen(SNAP_OFF_EMPTY_SCREEN_DURATION, FADE_EMPTY_SCREEN_DURATION,
-                        onComplete, stripEmptyScreens);
-            } else {
-                snapToPage(getNextPage(), 0);
-                fadeAndRemoveEmptyScreen(0, FADE_EMPTY_SCREEN_DURATION,
-                        onComplete, stripEmptyScreens);
-            }
-            return;
-        } else if (stripEmptyScreens) {
-            // If we're not going to strip the empty screens after removing
-            // the extra empty screen, do it right away.
+            removeView(mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID));
+            mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
+            mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
+
+            // Update the page indicator to reflect the removed page.
+            showPageIndicatorAtCurrentScroll();
+        }
+
+        if (stripEmptyScreens) {
             stripEmptyScreens();
         }
 
@@ -681,44 +671,6 @@
         }
     }
 
-    private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete,
-            final boolean stripEmptyScreens) {
-        // XXX: Do we need to update LM workspace screens below?
-        final CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID);
-
-        mRemoveEmptyScreenRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if (hasExtraEmptyScreen()) {
-                    mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
-                    mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
-                    removeView(cl);
-                    if (stripEmptyScreens) {
-                        stripEmptyScreens();
-                    }
-                    // Update the page indicator to reflect the removed page.
-                    showPageIndicatorAtCurrentScroll();
-                }
-            }
-        };
-
-        ObjectAnimator oa = ObjectAnimator.ofFloat(cl, ALPHA, 0f);
-        oa.setDuration(duration);
-        oa.setStartDelay(delay);
-        oa.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (mRemoveEmptyScreenRunnable != null) {
-                    mRemoveEmptyScreenRunnable.run();
-                }
-                if (onComplete != null) {
-                    onComplete.run();
-                }
-            }
-        });
-        oa.start();
-    }
-
     public boolean hasExtraEmptyScreen() {
         return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && getChildCount() > 1;
     }
@@ -805,8 +757,6 @@
             }
         }
 
-        boolean isInAccessibleDrag = mLauncher.getAccessibilityDelegate().isInAccessibleDrag();
-
         // We enforce at least one page to add new items to. In the case that we remove the last
         // such screen, we convert the last screen to the empty screen
         int minScreens = 1;
@@ -822,15 +772,9 @@
                 if (indexOfChild(cl) < currentPage) {
                     pageShift++;
                 }
-
-                if (isInAccessibleDrag) {
-                    cl.enableAccessibleDrag(false, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
-                }
-
                 removeView(cl);
             } else {
                 // if this is the last screen, convert it to the empty screen
-                mRemoveEmptyScreenRunnable = null;
                 mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, cl);
                 mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID);
             }
@@ -965,11 +909,14 @@
         onOverlayScrollChanged(0);
     }
 
+    public boolean hasOverlay() {
+        return mLauncherOverlay != null;
+    }
 
     private boolean isScrollingOverlay() {
         return mLauncherOverlay != null &&
-                ((mIsRtl && getUnboundedScrollX() > mMaxScrollX)
-                        || (!mIsRtl && getUnboundedScrollX() < mMinScrollX));
+                ((mIsRtl && getUnboundedScroll() > mMaxScroll)
+                        || (!mIsRtl && getUnboundedScroll() < mMinScroll));
     }
 
     @Override
@@ -1005,7 +952,7 @@
 
     public void showPageIndicatorAtCurrentScroll() {
         if (mPageIndicator != null) {
-            mPageIndicator.setScroll(getScrollX(), computeMaxScrollX());
+            mPageIndicator.setScroll(getScrollX(), computeMaxScroll());
         }
     }
 
@@ -1056,7 +1003,15 @@
             if (!mOverlayShown) {
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.LEFT, ContainerType.WORKSPACE, 0);
-                mLauncher.getStatsLogManager().logSwipeOnContainer(true, 0);
+                mLauncher.getStatsLogManager().logger()
+                        .withSrcState(LAUNCHER_STATE_HOME)
+                        .withDstState(LAUNCHER_STATE_HOME)
+                        .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                                .setWorkspace(
+                                        LauncherAtom.WorkspaceContainer.newBuilder()
+                                                .setPageIndex(0))
+                                .build())
+                        .log(LAUNCHER_SWIPELEFT);
             }
             mOverlayShown = true;
             // Not announcing the overlay page for accessibility since it announces itself.
@@ -1066,7 +1021,15 @@
                 if (!ued.isPreviousHomeGesture()) {
                     mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
-                    mLauncher.getStatsLogManager().logSwipeOnContainer(false, -1);
+                    mLauncher.getStatsLogManager().logger()
+                            .withSrcState(LAUNCHER_STATE_HOME)
+                            .withDstState(LAUNCHER_STATE_HOME)
+                            .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                                    .setWorkspace(
+                                            LauncherAtom.WorkspaceContainer.newBuilder()
+                                                    .setPageIndex(-1))
+                                    .build())
+                            .log(LAUNCHER_SWIPERIGHT);
                 }
             } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
                 // When arriving to 0 overscroll from non-zero overscroll, announce page for
@@ -1158,9 +1121,20 @@
     protected void notifyPageSwitchListener(int prevPage) {
         super.notifyPageSwitchListener(prevPage);
         if (prevPage != mCurrentPage) {
-            int swipeDirection = (prevPage < mCurrentPage) ? Action.Direction.RIGHT : Action.Direction.LEFT;
+            int swipeDirection = (prevPage < mCurrentPage)
+                    ? Action.Direction.RIGHT : Action.Direction.LEFT;
+            StatsLogManager.EventEnum event = (prevPage < mCurrentPage)
+                    ? LAUNCHER_SWIPERIGHT : LAUNCHER_SWIPELEFT;
             mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                     swipeDirection, ContainerType.WORKSPACE, prevPage);
+            mLauncher.getStatsLogManager().logger()
+                    .withSrcState(LAUNCHER_STATE_HOME)
+                    .withDstState(LAUNCHER_STATE_HOME)
+                    .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                            .setWorkspace(
+                                    LauncherAtom.WorkspaceContainer.newBuilder()
+                                            .setPageIndex(prevPage)).build())
+                    .log(event);
         }
     }
 
@@ -1238,10 +1212,8 @@
 
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        IBinder windowToken = getWindowToken();
-        mWallpaperOffset.setWindowToken(windowToken);
+        mWallpaperOffset.setWindowToken(getWindowToken());
         computeScroll();
-        mDragController.setWindowToken(windowToken);
     }
 
     protected void onDetachedFromWindow() {
@@ -1281,7 +1253,7 @@
 
     /** Returns whether a drag should be allowed to be started from the current workspace state. */
     public boolean workspaceIconsCanBeDragged() {
-        return mLauncher.getStateManager().getState().workspaceIconsCanBeDragged;
+        return mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
     }
 
     private void updateChildrenLayersEnabled() {
@@ -1351,7 +1323,7 @@
     }
 
     public void snapToPageFromOverView(int whichPage) {
-        snapToPage(whichPage, OVERVIEW_TRANSITION_MS, Interpolators.ZOOM_IN);
+        snapToPage(whichPage, OVERVIEW.getTransitionDuration(mLauncher), Interpolators.ZOOM_IN);
     }
 
     private void onStartStateTransition(LauncherState state) {
@@ -1384,23 +1356,22 @@
      * Sets the current workspace {@link LauncherState}, then animates the UI
      */
     @Override
-    public void setStateWithAnimation(LauncherState toState,
-            AnimatorSetBuilder builder, AnimationConfig config) {
+    public void setStateWithAnimation(
+            LauncherState toState, StateAnimationConfig config, PendingAnimation animation) {
         StateTransitionListener listener = new StateTransitionListener(toState);
-        mStateTransitionAnimation.setStateWithAnimation(toState, builder, config);
+        mStateTransitionAnimation.setStateWithAnimation(toState, config, animation);
 
         // Invalidate the pages now, so that we have the visible pages before the
         // animation is started
-        if (toState.hasMultipleVisiblePages) {
+        if (toState.hasFlag(FLAG_MULTI_PAGE)) {
             mForceDrawAdjacentPages = true;
         }
         invalidate(); // This will call dispatchDraw(), which calls getVisiblePages().
 
         ValueAnimator stepAnimator = ValueAnimator.ofFloat(0, 1);
         stepAnimator.addUpdateListener(listener);
-        stepAnimator.setDuration(config.duration);
         stepAnimator.addListener(listener);
-        builder.play(stepAnimator);
+        animation.add(stepAnimator);
     }
 
     public WorkspaceStateTransitionAnimation getStateTransitionAnimation() {
@@ -1409,7 +1380,10 @@
 
     public void updateAccessibilityFlags() {
         // TODO: Update the accessibility flags appropriately when dragging.
-        int accessibilityFlag = mLauncher.getStateManager().getState().workspaceAccessibilityFlag;
+        int accessibilityFlag =
+                mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_INACCESSIBLE)
+                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
         if (!mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
             int total = getPageCount();
             for (int i = 0; i < total; i++) {
@@ -1445,14 +1419,14 @@
         child.setVisibility(INVISIBLE);
 
         if (options.isAccessibleDrag) {
-            mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                    this, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG) {
-                @Override
-                protected void enableAccessibleDrag(boolean enable) {
-                    super.enableAccessibleDrag(enable);
-                    setEnableForLayout(mLauncher.getHotseat(),enable);
-                }
-            });
+            mDragController.addDragListener(
+                    new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
+                        @Override
+                        protected void enableAccessibleDrag(boolean enable) {
+                            super.enableAccessibleDrag(enable);
+                            setEnableForLayout(mLauncher.getHotseat(), enable);
+                        }
+                    });
         }
 
         beginDragShared(child, this, options);
@@ -1466,15 +1440,17 @@
                     + "View: " + child + "  tag: " + child.getTag();
             throw new IllegalStateException(msg);
         }
-        beginDragShared(child, source, (ItemInfo) dragObject,
+        beginDragShared(child, null, source, (ItemInfo) dragObject,
                 new DragPreviewProvider(child), options);
     }
 
-    public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
-            DragPreviewProvider previewProvider, DragOptions dragOptions) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "beginDragShared");
-        }
+    /**
+     * Core functionality for beginning a drag operation for an item that will be dropped within
+     * the workspace
+     */
+    public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
+            ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
+
         float iconScale = 1f;
         if (child instanceof BubbleTextView) {
             Drawable icon = ((BubbleTextView) child).getIcon();
@@ -1483,42 +1459,37 @@
             }
         }
 
+        // Clear the pressed state if necessary
         child.clearFocus();
         child.setPressed(false);
-        mOutlineProvider = previewProvider;
-
-        // The drag bitmap follows the touch point around on the screen
-        final Bitmap b = previewProvider.createDragBitmap();
-        int halfPadding = previewProvider.previewPadding / 2;
-
-        float scale = previewProvider.getScaleAndPosition(b, mTempXY);
-        int dragLayerX = mTempXY[0];
-        int dragLayerY = mTempXY[1];
-
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        Point dragVisualizeOffset = null;
-        Rect dragRect = null;
-        if (child instanceof BubbleTextView) {
-            dragRect = new Rect();
-            BubbleTextView.getIconBounds(child, dragRect, grid.iconSizePx);
-            dragLayerY += dragRect.top;
-            // Note: The dragRect is used to calculate drag layer offsets, but the
-            // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
-        } else if (child instanceof FolderIcon) {
-            int previewSize = grid.folderIconSizePx;
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
-            dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
-        } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
-        }
-
-        // Clear the pressed state if necessary
         if (child instanceof BubbleTextView) {
             BubbleTextView icon = (BubbleTextView) child;
             icon.clearPressedBackground();
         }
 
+        mOutlineProvider = previewProvider;
+
+        if (draggableView == null && child instanceof DraggableView) {
+            draggableView = (DraggableView) child;
+        }
+
+        // The drag bitmap follows the touch point around on the screen
+        final Bitmap b = previewProvider.createDragBitmap();
+        int halfPadding = previewProvider.previewPadding / 2;
+        float scale = previewProvider.getScaleAndPosition(b, mTempXY);
+        int dragLayerX = mTempXY[0];
+        int dragLayerY = mTempXY[1];
+
+        Point dragVisualizeOffset = null;
+        Rect dragRect = new Rect();
+
+        if (draggableView != null) {
+            draggableView.getSourceVisualDragBounds(dragRect);
+            dragLayerY += dragRect.top;
+            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
+        }
+
+
         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
             mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
         }
@@ -1528,13 +1499,13 @@
                     .showForIcon((BubbleTextView) child);
             if (popupContainer != null) {
                 dragOptions.preDragCondition = popupContainer.createPreDragCondition();
-
                 mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
             }
         }
 
-        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
-                dragObject, dragVisualizeOffset, dragRect, scale * iconScale, scale, dragOptions);
+        DragView dv = mDragController.startDrag(b, draggableView, dragLayerX, dragLayerY, source,
+                dragObject, dragVisualizeOffset, dragRect, scale * iconScale,
+                scale, dragOptions);
         dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
         return dv;
     }
@@ -1641,7 +1612,9 @@
             return false;
         }
 
-        boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo);
+        boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo
+                && ((WorkspaceItemInfo) dropOverView.getTag()).container
+                != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
         boolean willBecomeShortcut =
                 (info.itemType == ITEM_TYPE_APPLICATION ||
                         info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
@@ -1675,7 +1648,7 @@
     }
 
     boolean createUserFolderIfNecessary(View newView, int container, CellLayout target,
-            int[] targetCell, float distance, boolean external, DragView dragView) {
+            int[] targetCell, float distance, boolean external, DragObject d) {
         if (distance > mMaxDistanceForFolderCreation) return false;
         View v = target.getChildAt(targetCell[0], targetCell[1]);
 
@@ -1704,28 +1677,29 @@
             Rect folderLocation = new Rect();
             float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
             target.removeView(v);
-
-            FolderIcon fi =
-                mLauncher.addFolder(target, container, screenId, targetCell[0], targetCell[1]);
+            mStatsLogManager.logger().withItemInfo(destInfo).withInstanceId(d.logInstanceId)
+                    .log(LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED);
+            FolderIcon fi = mLauncher.addFolder(target, container, screenId, targetCell[0],
+                    targetCell[1]);
             destInfo.cellX = -1;
             destInfo.cellY = -1;
             sourceInfo.cellX = -1;
             sourceInfo.cellY = -1;
 
             // If the dragView is null, we can't animate
-            boolean animate = dragView != null;
+            boolean animate = d != null;
             if (animate) {
                 // In order to keep everything continuous, we hand off the currently rendered
                 // folder background to the newly created icon. This preserves animation state.
                 fi.setFolderBackground(mFolderCreateBg);
                 mFolderCreateBg = new PreviewBackground();
-                fi.performCreateAnimation(destInfo, v, sourceInfo, dragView, folderLocation, scale
-                );
+                fi.performCreateAnimation(destInfo, v, sourceInfo, d, folderLocation, scale);
             } else {
                 fi.prepareCreateAnimation(v);
                 fi.addItem(destInfo);
                 fi.addItem(sourceInfo);
             }
+            mLauncher.folderCreatedFromItem(fi.getFolder(), destInfo);
             return true;
         }
         return false;
@@ -1742,6 +1716,8 @@
         if (dropOverView instanceof FolderIcon) {
             FolderIcon fi = (FolderIcon) dropOverView;
             if (fi.acceptDrop(d.dragInfo)) {
+                mStatsLogManager.logger().withItemInfo(fi.mInfo).withInstanceId(d.logInstanceId)
+                        .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
                 fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
 
                 // if the drag started here, we need to remove it from the workspace
@@ -1757,10 +1733,8 @@
     @Override
     public void prepareAccessibilityDrop() { }
 
+    @Override
     public void onDrop(final DragObject d, DragOptions options) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDrop");
-        }
         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
         CellLayout dropTargetLayout = mDropToLayout;
 
@@ -1804,8 +1778,8 @@
                 // If the item being dropped is a shortcut and the nearest drop
                 // cell also contains a shortcut, then create a folder with the two shortcuts.
                 if (createUserFolderIfNecessary(cell, container,
-                        dropTargetLayout, mTargetCell, distance, false, d.dragView) ||
-                        addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
+                        dropTargetLayout, mTargetCell, distance, false, d)
+                        || addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                                 distance, d, false)) {
                     mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                     return;
@@ -1864,7 +1838,7 @@
                         CellLayout parentCell = getParentCellLayoutForView(cell);
                         if (parentCell != null) {
                             parentCell.removeView(cell);
-                        } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                        } else if (FeatureFlags.IS_STUDIO_BUILD) {
                             throw new NullPointerException("mDragInfo.cell has null parent");
                         }
                         addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
@@ -1888,12 +1862,10 @@
                         final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
                         if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
-                                && !d.accessibleDrag) {
-                            onCompleteRunnable = new Runnable() {
-                                public void run() {
-                                    if (!isPageInTransition()) {
-                                        AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
-                                    }
+                                && !options.isAccessibleDrag) {
+                            onCompleteRunnable = () -> {
+                                if (!isPageInTransition()) {
+                                    AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
                                 }
                             };
                         }
@@ -1921,7 +1893,8 @@
                     // Animate the item to its original position, while simultaneously exiting
                     // spring-loaded mode so the page meets the icon where it was picked up.
                     mLauncher.getDragController().animateDragViewToOriginalPosition(
-                            onCompleteRunnable, cell, SPRING_LOADED_TRANSITION_MS);
+                            onCompleteRunnable, cell,
+                            SPRING_LOADED.getTransitionDuration(mLauncher));
                     mLauncher.getStateManager().goToState(NORMAL);
                     mLauncher.getDropTargetBar().onDragEnd();
                     parent.onDropChild(cell);
@@ -1947,6 +1920,8 @@
 
             mLauncher.getStateManager().goToState(
                     NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
+            mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
+                    .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
         }
 
         if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
@@ -2092,8 +2067,6 @@
         if (mFolderCreateBg != null) {
             mFolderCreateBg.animateToRest();
         }
-        mFolderCreationAlarm.setOnAlarmListener(null);
-        mFolderCreationAlarm.cancelAlarm();
     }
 
     private void cleanupAddToFolder() {
@@ -2158,7 +2131,7 @@
 
         ItemInfo item = d.dragInfo;
         if (item == null) {
-            if (FeatureFlags.IS_DOGFOOD_BUILD) {
+            if (FeatureFlags.IS_STUDIO_BUILD) {
                 throw new NullPointerException("DragObject has null info");
             }
             return;
@@ -2200,14 +2173,14 @@
             float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                     mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
 
-            manageFolderFeedback(mDragTargetLayout, mTargetCell, targetCellDistance, d);
+            manageFolderFeedback(targetCellDistance, d);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                     mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
                     item.spanY, child, mTargetCell);
 
             if (!nearestDropOccupied) {
-                mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
+                mDragTargetLayout.visualizeDropLocation(d.originalView, mOutlineProvider,
                         mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
             } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                     && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
@@ -2298,10 +2271,10 @@
         return null;
     }
 
-    private void manageFolderFeedback(CellLayout targetLayout,
-            int[] targetCell, float distance, DragObject dragObject) {
+    private void manageFolderFeedback(float distance, DragObject dragObject) {
         if (distance > mMaxDistanceForFolderCreation) {
-            if (mDragMode != DRAG_MODE_NONE) {
+            if ((mDragMode == DRAG_MODE_ADD_TO_FOLDER
+                    || mDragMode == DRAG_MODE_CREATE_FOLDER)) {
                 setDragMode(DRAG_MODE_NONE);
             }
             return;
@@ -2310,18 +2283,18 @@
         final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
         ItemInfo info = dragObject.dragInfo;
         boolean userFolderPending = willCreateUserFolder(info, dragOverView, false);
-        if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
-                !mFolderCreationAlarm.alarmPending()) {
+        if (mDragMode == DRAG_MODE_NONE && userFolderPending) {
 
-            FolderCreationAlarmListener listener = new
-                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
+            mFolderCreateBg = new PreviewBackground();
+            mFolderCreateBg.setup(mLauncher, mLauncher, null,
+                    dragOverView.getMeasuredWidth(), dragOverView.getPaddingTop());
 
-            if (!dragObject.accessibleDrag) {
-                mFolderCreationAlarm.setOnAlarmListener(listener);
-                mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
-            } else {
-                listener.onAlarm(mFolderCreationAlarm);
-            }
+            // The full preview background should appear behind the icon
+            mFolderCreateBg.isClipping = false;
+
+            mFolderCreateBg.animateToAccept(mDragTargetLayout, mTargetCell[0], mTargetCell[1]);
+            mDragTargetLayout.clearDragOutlines();
+            setDragMode(DRAG_MODE_CREATE_FOLDER);
 
             if (dragObject.stateAnnouncer != null) {
                 dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
@@ -2334,8 +2307,8 @@
         if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
             mDragOverFolderIcon = ((FolderIcon) dragOverView);
             mDragOverFolderIcon.onDragEnter(info);
-            if (targetLayout != null) {
-                targetLayout.clearDragOutlines();
+            if (mDragTargetLayout != null) {
+                mDragTargetLayout.clearDragOutlines();
             }
             setDragMode(DRAG_MODE_ADD_TO_FOLDER);
 
@@ -2354,33 +2327,6 @@
         }
     }
 
-    class FolderCreationAlarmListener implements OnAlarmListener {
-        final CellLayout layout;
-        final int cellX;
-        final int cellY;
-
-        final PreviewBackground bg = new PreviewBackground();
-
-        public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) {
-            this.layout = layout;
-            this.cellX = cellX;
-            this.cellY = cellY;
-
-            BubbleTextView cell = (BubbleTextView) layout.getChildAt(cellX, cellY);
-            bg.setup(mLauncher, mLauncher, null, cell.getMeasuredWidth(), cell.getPaddingTop());
-
-            // The full preview background should appear behind the icon
-            bg.isClipping = false;
-        }
-
-        public void onAlarm(Alarm alarm) {
-            mFolderCreateBg = bg;
-            mFolderCreateBg.animateToAccept(layout, cellX, cellY);
-            layout.clearDragOutlines();
-            setDragMode(DRAG_MODE_CREATE_FOLDER);
-        }
-    }
-
     class ReorderAlarmListener implements OnAlarmListener {
         final float[] dragViewCenter;
         final int minSpanX, minSpanY, spanX, spanY;
@@ -2417,7 +2363,7 @@
             }
 
             boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
-            mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
+            mDragTargetLayout.visualizeDropLocation(dragObject.originalView, mOutlineProvider,
                 mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
         }
     }
@@ -2438,9 +2384,6 @@
      * to add an item to one of the workspace screens.
      */
     private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDropExternal");
-        }
         if (d.dragInfo instanceof PendingAddShortcutInfo) {
             WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
                     .activityInfo.createWorkspaceItemInfo();
@@ -2516,6 +2459,9 @@
                     // widgets/shortcuts/folders in a slightly different way
                     mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell,
                             item.spanX, item.spanY);
+                    mStatsLogManager.logger().withItemInfo(d.dragInfo)
+                            .withInstanceId(d.logInstanceId)
+                            .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
                 }
             };
             boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
@@ -2554,7 +2500,7 @@
                     view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
                     break;
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                    view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
                             (FolderInfo) info);
                     break;
                 default:
@@ -2569,7 +2515,7 @@
                 float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                         mDragViewVisualCenter[1], mTargetCell);
                 if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance,
-                        true, d.dragView)) {
+                        true, d)) {
                     return;
                 }
                 if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d,
@@ -2604,7 +2550,10 @@
                 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, this);
                 resetTransitionTransform();
             }
+            mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
+                    .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
         }
+
     }
 
     public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
@@ -2614,11 +2563,10 @@
 
         int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY);
         int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY);
-        Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1],
-                Bitmap.Config.ARGB_8888);
         layout.measure(width, height);
         layout.layout(0, 0, unScaledSize[0], unScaledSize[1]);
-        layout.draw(new Canvas(b));
+        Bitmap b = BitmapRenderer.createHardwareBitmap(
+                unScaledSize[0], unScaledSize[1], layout::draw);
         layout.setVisibility(visibility);
         return b;
     }
@@ -2784,7 +2732,7 @@
                     mDragInfo.container, mDragInfo.screenId);
             if (cellLayout != null) {
                 cellLayout.onDropChild(mDragInfo.cell);
-            } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
+            } else if (FeatureFlags.IS_STUDIO_BUILD) {
                 throw new RuntimeException("Invalid state: cellLayout == null in "
                         + "Workspace#onDropCompleted. Please file a bug. ");
             }
@@ -2803,7 +2751,7 @@
         CellLayout parentCell = getParentCellLayoutForView(v);
         if (parentCell != null) {
             parentCell.removeView(v);
-        } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
+        } else if (FeatureFlags.IS_STUDIO_BUILD) {
             // When an app is uninstalled using the drop target, we wait until resume to remove
             // the icon. We also remove all the corresponding items from the workspace at
             // {@link Launcher#bindComponentsRemoved}. That call can come before or after
@@ -3231,12 +3179,11 @@
 
             LauncherAppWidgetInfo item = changedInfo.get(0);
             final AppWidgetProviderInfo widgetInfo;
+            WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext());
             if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-                widgetInfo = AppWidgetManagerCompat
-                        .getInstance(mLauncher).findProvider(item.providerName, item.user);
+                widgetInfo = widgetHelper.findProvider(item.providerName, item.user);
             } else {
-                widgetInfo = AppWidgetManagerCompat.getInstance(mLauncher)
-                        .getLauncherAppWidgetInfo(item.appWidgetId);
+                widgetInfo = widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId);
             }
 
             if (widgetInfo != null) {
@@ -3265,7 +3212,8 @@
         return mOverlayShown;
     }
 
-    void moveToDefaultScreen() {
+    /** Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it. */
+    public void moveToDefaultScreen() {
         int page = DEFAULT_PAGE;
         if (!workspaceInModalState() && getNextPage() != page) {
             snapToPage(page);
@@ -3318,17 +3266,21 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        target.gridX = info.cellX;
-        target.gridY = info.cellY;
-        target.pageIndex = getCurrentPage();
-        targetParent.containerType = ContainerType.WORKSPACE;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-            target.rank = info.rank;
-            targetParent.containerType = ContainerType.HOTSEAT;
-        } else if (info.container >= 0) {
-            targetParent.containerType = ContainerType.FOLDER;
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        if (childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            getHotseat().fillInLogContainerData(childInfo, child, parents);
+            return;
+        } else if (childInfo.container >= 0) {
+            FolderIcon icon = (FolderIcon) getHomescreenIconByItemId(childInfo.container);
+            icon.getFolder().fillInLogContainerData(childInfo, child, parents);
+            return;
         }
+        child.gridX = childInfo.cellX;
+        child.gridY = childInfo.cellY;
+        child.pageIndex = getCurrentPage();
+        parents.add(newContainerTarget(ContainerType.WORKSPACE));
     }
 
     /**
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index ea2d4d0..c3d4aeb 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -21,6 +21,7 @@
 
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.touch.ItemLongClickListener;
 
 public interface WorkspaceLayoutManager {
@@ -39,7 +40,8 @@
     default void addInScreenFromBind(View child, ItemInfo info) {
         int x = info.cellX;
         int y = info.cellY;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             int screenId = info.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
@@ -83,7 +85,8 @@
         }
 
         final CellLayout layout;
-        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             layout = getHotseat();
 
             // Hide folder title in the hotseat
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 7a7e1fe..cd938e1 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -16,29 +16,43 @@
 
 package com.android.launcher3;
 
+import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE;
+
 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherState.FLAG_HAS_SYS_UI_SCRIM;
+import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_HAS_BACKGROUNDS;
+import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.LauncherState.NORMAL;
 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.WorkspaceAndHotseatScrim.SCRIM_PROGRESS;
+import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
 import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SYSUI_PROGRESS;
+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;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 
+import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.LauncherState.PageAlphaProvider;
 import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.allapps.AllAppsContainerView;
+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.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
+import com.android.systemui.plugins.ResourceProvider;
 
 /**
  * Manages the animations between each of the workspace states.
@@ -56,13 +70,15 @@
     }
 
     public void setState(LauncherState toState) {
-        setWorkspaceProperty(toState, NO_ANIM_PROPERTY_SETTER, new AnimatorSetBuilder(),
-                new AnimationConfig());
+        setWorkspaceProperty(toState, NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig());
     }
 
-    public void setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder,
-            AnimationConfig config) {
-        setWorkspaceProperty(toState, config.getPropertySetter(builder), builder, config);
+    /**
+     * @see com.android.launcher3.statemanager.StateManager.StateHandler#setStateWithAnimation
+     */
+    public void setStateWithAnimation(
+            LauncherState toState, StateAnimationConfig config, PendingAnimation animation) {
+        setWorkspaceProperty(toState, animation, config);
     }
 
     public float getFinalScale() {
@@ -73,41 +89,55 @@
      * Starts a transition animation for the workspace.
      */
     private void setWorkspaceProperty(LauncherState state, PropertySetter propertySetter,
-            AnimatorSetBuilder builder, AnimationConfig config) {
+            StateAnimationConfig config) {
         ScaleAndTranslation scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher);
         ScaleAndTranslation hotseatScaleAndTranslation = state.getHotseatScaleAndTranslation(
                 mLauncher);
+        ScaleAndTranslation qsbScaleAndTranslation = state.getQsbScaleAndTranslation(mLauncher);
         mNewScale = scaleAndTranslation.scale;
         PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher);
         final int childCount = mWorkspace.getChildCount();
         for (int i = 0; i < childCount; i++) {
             applyChildState(state, (CellLayout) mWorkspace.getChildAt(i), i, pageAlphaProvider,
-                    propertySetter, builder, config);
+                    propertySetter, config);
         }
 
         int elements = state.getVisibleElements(mLauncher);
-        Interpolator fadeInterpolator = builder.getInterpolator(ANIM_WORKSPACE_FADE,
+        Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE,
                 pageAlphaProvider.interpolator);
         boolean playAtomicComponent = config.playAtomicOverviewScaleComponent();
         Hotseat hotseat = mWorkspace.getHotseat();
+        // Since we set the pivot relative to mWorkspace, we need to scale a sibling of Workspace.
+        AllAppsContainerView qsbScaleView = mLauncher.getAppsView();
+        View qsbView = qsbScaleView.getSearchView();
         if (playAtomicComponent) {
-            Interpolator scaleInterpolator = builder.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
-            propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
-
-            if (!hotseat.getRotationMode().isTransposed) {
-                // Set the hotseat's pivot point to match the workspace's, so that it scales
-                // together. Since both hotseat and workspace can move, transform the point
-                // manually instead of using dragLayer.getDescendantCoordRelativeToSelf and
-                // related methods.
-                hotseat.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() - hotseat.getTop());
-                hotseat.setPivotX(mWorkspace.getPivotX()
-                        + mWorkspace.getLeft() - hotseat.getLeft());
+            Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
+            LauncherState fromState = mLauncher.getStateManager().getState();
+            boolean shouldSpring = propertySetter instanceof PendingAnimation
+                    && fromState == HINT_STATE && state == NORMAL;
+            if (shouldSpring) {
+                ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher,
+                        mWorkspace, mNewScale));
+            } else {
+                propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
             }
+
+            setPivotToScaleWithWorkspace(hotseat);
+            setPivotToScaleWithWorkspace(qsbScaleView);
             float hotseatScale = hotseatScaleAndTranslation.scale;
-            Interpolator hotseatScaleInterpolator = builder.getInterpolator(ANIM_HOTSEAT_SCALE,
-                    scaleInterpolator);
-            propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
-                    hotseatScaleInterpolator);
+            if (shouldSpring) {
+                PendingAnimation pa = (PendingAnimation) propertySetter;
+                pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale));
+                pa.add(getSpringScaleAnimator(mLauncher, qsbScaleView,
+                        qsbScaleAndTranslation.scale));
+            } else {
+                Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
+                        scaleInterpolator);
+                propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
+                        hotseatScaleInterpolator);
+                propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
+                        hotseatScaleInterpolator);
+            }
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
@@ -115,56 +145,93 @@
                     hotseatIconsAlpha, fadeInterpolator);
         }
 
-        if (!config.playNonAtomicComponent()) {
+        if (config.onlyPlayAtomicComponent()) {
             // Only the alpha and scale, handled above, are included in the atomic animation.
             return;
         }
 
         Interpolator translationInterpolator = !playAtomicComponent
                 ? LINEAR
-                : builder.getInterpolator(ANIM_WORKSPACE_TRANSLATE, ZOOM_OUT);
-        propertySetter.setFloat(mWorkspace, View.TRANSLATION_X,
+                : config.getInterpolator(ANIM_WORKSPACE_TRANSLATE, ZOOM_OUT);
+        propertySetter.setFloat(mWorkspace, VIEW_TRANSLATE_X,
                 scaleAndTranslation.translationX, translationInterpolator);
-        propertySetter.setFloat(mWorkspace, View.TRANSLATION_Y,
+        propertySetter.setFloat(mWorkspace, VIEW_TRANSLATE_Y,
                 scaleAndTranslation.translationY, translationInterpolator);
 
-        Interpolator hotseatTranslationInterpolator = builder.getInterpolator(
+        Interpolator hotseatTranslationInterpolator = config.getInterpolator(
                 ANIM_HOTSEAT_TRANSLATE, translationInterpolator);
-        propertySetter.setFloat(hotseat, View.TRANSLATION_Y,
+        propertySetter.setFloat(hotseat, VIEW_TRANSLATE_Y,
                 hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
-        propertySetter.setFloat(mWorkspace.getPageIndicator(), View.TRANSLATION_Y,
+        propertySetter.setFloat(mWorkspace.getPageIndicator(), VIEW_TRANSLATE_Y,
                 hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
+        propertySetter.setFloat(qsbView, VIEW_TRANSLATE_Y,
+                qsbScaleAndTranslation.translationY, hotseatTranslationInterpolator);
 
         setScrim(propertySetter, state);
     }
 
+    /**
+     * Set the given view's pivot point to match the workspace's, so that it scales together. Since
+     * both this view and workspace can move, transform the point manually instead of using
+     * dragLayer.getDescendantCoordRelativeToSelf and related methods.
+     */
+    private void setPivotToScaleWithWorkspace(View sibling) {
+        sibling.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop()
+                - sibling.getTop() - sibling.getTranslationY());
+        sibling.setPivotX(mWorkspace.getPivotX() + mWorkspace.getLeft()
+                - sibling.getLeft() - sibling.getTranslationX());
+    }
+
     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, state.hasSysUiScrim ? 1 : 0, LINEAR);
+        propertySetter.setFloat(scrim, SYSUI_PROGRESS,
+                state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
     }
 
     public void applyChildState(LauncherState state, CellLayout cl, int childIndex) {
         applyChildState(state, cl, childIndex, state.getWorkspacePageAlphaProvider(mLauncher),
-                NO_ANIM_PROPERTY_SETTER, new AnimatorSetBuilder(), new AnimationConfig());
+                NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig());
     }
 
     private void applyChildState(LauncherState state, CellLayout cl, int childIndex,
             PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter,
-            AnimatorSetBuilder builder, AnimationConfig config) {
+            StateAnimationConfig config) {
         float pageAlpha = pageAlphaProvider.getPageAlpha(childIndex);
-        int drawableAlpha = Math.round(pageAlpha * (state.hasWorkspacePageBackground ? 255 : 0));
+        int drawableAlpha = state.hasFlag(FLAG_WORKSPACE_HAS_BACKGROUNDS)
+                ? Math.round(pageAlpha * 255) : 0;
 
-        if (config.playNonAtomicComponent()) {
+        if (!config.onlyPlayAtomicComponent()) {
+            // Don't update the scrim during the atomic animation.
             propertySetter.setInt(cl.getScrimBackground(),
                     DRAWABLE_ALPHA, drawableAlpha, ZOOM_OUT);
         }
         if (config.playAtomicOverviewScaleComponent()) {
-            Interpolator fadeInterpolator = builder.getInterpolator(ANIM_WORKSPACE_FADE,
+            Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE,
                     pageAlphaProvider.interpolator);
-            propertySetter.setFloat(cl.getShortcutsAndWidgets(), View.ALPHA,
+            propertySetter.setFloat(cl.getShortcutsAndWidgets(), VIEW_ALPHA,
                     pageAlpha, fadeInterpolator);
         }
     }
+
+    /**
+     * Returns a spring based animator for the scale property of {@param v}.
+     */
+    public static ValueAnimator getSpringScaleAnimator(Launcher launcher, View v, float scale) {
+        ResourceProvider rp = DynamicResource.provider(launcher);
+        float damping = rp.getFloat(R.dimen.hint_scale_damping_ratio);
+        float stiffness = rp.getFloat(R.dimen.hint_scale_stiffness);
+        float velocityPxPerS = rp.getDimension(R.dimen.hint_scale_velocity_dp_per_s);
+
+        return new SpringAnimationBuilder(v.getContext())
+                .setStiffness(stiffness)
+                .setDampingRatio(damping)
+                .setMinimumVisibleChange(MIN_VISIBLE_CHANGE_SCALE)
+                .setEndValue(scale)
+                .setStartValue(SCALE_PROPERTY.get(v))
+                .setStartVelocity(velocityPxPerS)
+                .build(v, SCALE_PROPERTY);
+
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
index f8df5d7..0d7df2b 100644
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
@@ -16,7 +16,9 @@
 
 package com.android.launcher3.accessibility;
 
+import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewGroup.OnHierarchyChangeListener;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
@@ -24,36 +26,55 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
 
+import java.util.function.Function;
+
 /**
  * Utility listener to enable/disable accessibility drag flags for a ViewGroup
  * containing CellLayouts
  */
-public class AccessibleDragListenerAdapter implements DragListener {
+public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
 
     private final ViewGroup mViewGroup;
-    private final int mDragType;
+    private final Function<CellLayout, DragAndDropAccessibilityDelegate> mDelegateFactory;
 
     /**
-     * @param parent
-     * @param dragType either {@link CellLayout#WORKSPACE_ACCESSIBILITY_DRAG} or
-     *                 {@link CellLayout#FOLDER_ACCESSIBILITY_DRAG}
+     * @param parent the viewgroup containing all the children
+     * @param delegateFactory function to create no delegates
      */
-    public AccessibleDragListenerAdapter(ViewGroup parent, int dragType) {
+    public AccessibleDragListenerAdapter(ViewGroup parent,
+            Function<CellLayout, DragAndDropAccessibilityDelegate> delegateFactory) {
         mViewGroup = parent;
-        mDragType = dragType;
+        mDelegateFactory = delegateFactory;
     }
 
     @Override
     public void onDragStart(DragObject dragObject, DragOptions options) {
+        mViewGroup.setOnHierarchyChangeListener(this);
         enableAccessibleDrag(true);
     }
 
     @Override
     public void onDragEnd() {
+        mViewGroup.setOnHierarchyChangeListener(null);
         enableAccessibleDrag(false);
         Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
     }
 
+
+    @Override
+    public void onChildViewAdded(View parent, View child) {
+        if (parent == mViewGroup) {
+            setEnableForLayout((CellLayout) child, true);
+        }
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+        if (parent == mViewGroup) {
+            setEnableForLayout((CellLayout) child, false);
+        }
+    }
+
     protected void enableAccessibleDrag(boolean enable) {
         for (int i = 0; i < mViewGroup.getChildCount(); i++) {
             setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
@@ -61,6 +82,6 @@
     }
 
     protected final void setEnableForLayout(CellLayout layout, boolean enable) {
-        layout.enableAccessibleDrag(enable, mDragType);
+        layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
     }
 }
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index 117296d..ddb547f 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -19,24 +19,26 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.View.OnHoverListener;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.customview.widget.ExploreByTouchHelper;
+
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 
 import java.util.List;
 
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.customview.widget.ExploreByTouchHelper;
-
 /**
  * Helper class to make drag-and-drop in a {@link CellLayout} accessible.
  */
 public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper
-        implements OnClickListener {
+        implements OnClickListener, OnHoverListener {
     protected static final int INVALID_POSITION = -1;
 
     private static final int[] sTempArray = new int[2];
@@ -123,6 +125,11 @@
         node.setFocusable(true);
     }
 
+    @Override
+    public boolean onHover(View view, MotionEvent motionEvent) {
+        return dispatchHoverEvent(motionEvent);
+    }
+
     protected abstract String getLocationDescriptionForIconDrop(int id);
 
     protected abstract String getConfirmationForIconDrop(int id);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 0c12c60..136d43e 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -7,6 +7,7 @@
 import android.app.AlertDialog;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.DialogInterface;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
@@ -18,26 +19,26 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.ButtonDropTarget;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.WorkspaceItemInfo;
 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.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.PopupContainerWithArrow;
 import com.android.launcher3.touch.ItemLongClickListener;
@@ -54,6 +55,8 @@
 
     public static final int REMOVE = R.id.action_remove;
     public static final int UNINSTALL = R.id.action_uninstall;
+    public static final int DISMISS_PREDICTION = R.id.action_dismiss_prediction;
+    public static final int PIN_PREDICTION = R.id.action_pin_prediction;
     public static final int RECONFIGURE = R.id.action_reconfigure;
     protected static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace;
     protected static final int MOVE = R.id.action_move;
@@ -86,6 +89,8 @@
                 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,
@@ -116,6 +121,10 @@
         if (!(host.getTag() instanceof ItemInfo)) return;
         ItemInfo item = (ItemInfo) host.getTag();
 
+        if (host instanceof AccessibilityActionHandler) {
+            ((AccessibilityActionHandler) host).addSupportedAccessibilityActions(info);
+        }
+
         // 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)) {
@@ -142,15 +151,25 @@
             }
         }
 
+        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));
         }
     }
 
+    private boolean itemSupportsLongClick(View host, ItemInfo info) {
+        return PopupContainerWithArrow.canShow(host, info)
+                || new CustomActionsPopup(mLauncher, host).canShow();
+    }
+
     private boolean itemSupportsAccessibleDrag(ItemInfo item) {
         if (item instanceof WorkspaceItemInfo) {
             // Support the action unless the item is in a context menu.
-            return item.screenId >= 0;
+            return item.screenId >= 0 && item.container != Favorites.CONTAINER_HOTSEAT_PREDICTION;
         }
         return (item instanceof LauncherAppWidgetInfo)
                 || (item instanceof FolderInfo);
@@ -166,13 +185,25 @@
     }
 
     public boolean performAction(final View host, final ItemInfo item, int action) {
-        if (action == ACTION_LONG_CLICK && ShortcutUtil.isDeepShortcut(item)) {
-            CustomActionsPopup popup = new CustomActionsPopup(mLauncher, host);
-            if (popup.canShow()) {
-                popup.show();
+        if (action == ACTION_LONG_CLICK) {
+            if (PopupContainerWithArrow.canShow(host, item)) {
+                // Long press should be consumed for workspace items, and it should invoke the
+                // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the
+                // 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 == ADD_TO_WORKSPACE) {
@@ -191,6 +222,7 @@
                         ArrayList<ItemInfo> itemList = new ArrayList<>();
                         itemList.add(info);
                         mLauncher.bindItems(itemList, true);
+                        announceConfirmation(R.string.item_added_to_workspace);
                     } else if (item instanceof PendingAddItemInfo) {
                         PendingAddItemInfo info = (PendingAddItemInfo) item;
                         Workspace workspace = mLauncher.getWorkspace();
@@ -198,7 +230,6 @@
                         mLauncher.addPendingItem(info, Favorites.CONTAINER_DESKTOP,
                                 screenId, coordinates, info.spanX, info.spanY);
                     }
-                    announceConfirmation(R.string.item_added_to_workspace);
                 }
             });
             return true;
@@ -388,11 +419,11 @@
 
         Rect pos = new Rect();
         mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
-        mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY());
         mLauncher.getDragController().addDragListener(this);
 
         DragOptions options = new DragOptions();
         options.isAccessibleDrag = true;
+        options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
         ItemLongClickListener.beginDrag(item, mLauncher, info, options);
     }
 
@@ -443,4 +474,20 @@
         }
         return screenId;
     }
+
+    /**
+     * An interface allowing views to handle their own action.
+     */
+    public interface AccessibilityActionHandler {
+
+        /**
+         * performs accessibility action and returns true on success
+         */
+        boolean performAccessibilityAction(int action, ItemInfo itemInfo);
+
+        /**
+         * adds all the accessibility actions that can be handled.
+         */
+        void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo);
+    }
 }
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index c0c0b37..d4ba11e 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -23,11 +23,11 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationMainView;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 17daeb8..65a261d 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -21,17 +21,17 @@
 import android.text.TextUtils;
 import android.view.View;
 
-import com.android.launcher3.AppInfo;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType;
 import com.android.launcher3.dragndrop.DragLayer;
-
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+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.WorkspaceItemInfo;
 
 /**
  * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD on workspace.
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 2d5b040..c989e7b 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -15,13 +15,19 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+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;
+
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.os.Bundle;
 import android.os.Process;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
@@ -32,38 +38,37 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
-import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
-import com.android.launcher3.AppInfo;
+
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 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.BottomUserEducationView;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.SpringRelativeLayout;
 
+import java.util.ArrayList;
+
 /**
  * The all apps view container.
  */
@@ -75,8 +80,8 @@
     private static final float FLING_ANIMATION_THRESHOLD = 0.55f;
     private static final int ALPHA_CHANNEL_COUNT = 2;
 
-    private final Launcher mLauncher;
-    private final AdapterHolder[] mAH;
+    protected final BaseDraggingActivity mLauncher;
+    protected final AdapterHolder[] mAH;
     private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
     private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
     private final AllAppsStore mAllAppsStore = new AllAppsStore();
@@ -84,21 +89,26 @@
     private final Paint mNavBarScrimPaint;
     private int mNavBarScrimHeight = 0;
 
-    private SearchUiManager mSearchUiManager;
+    protected SearchUiManager mSearchUiManager;
     private View mSearchContainer;
     private AllAppsPagedView mViewPager;
+
     private FloatingHeaderView mHeader;
+    private WorkModeSwitch mWorkModeSwitch;
+
 
     private SpannableStringBuilder mSearchQueryBuilder = null;
 
-    private boolean mUsingTabs;
+    protected boolean mUsingTabs;
     private boolean mSearchModeWhileUsingTabs = false;
 
-    private RecyclerViewFastScroller mTouchHandler;
-    private final Point mFastScrollerOffset = new Point();
+    protected RecyclerViewFastScroller mTouchHandler;
+    protected final Point mFastScrollerOffset = new Point();
 
     private final MultiValueAlpha mMultiValueAlpha;
 
+    Rect mInsets = new Rect();
+
     public AllAppsContainerView(Context context) {
         this(context, null);
     }
@@ -110,7 +120,7 @@
     public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
-        mLauncher = Launcher.getLauncher(context);
+        mLauncher = BaseDraggingActivity.fromContext(context);
         mLauncher.addOnDeviceProfileChangeListener(this);
 
         mSearchQueryBuilder = new SpannableStringBuilder();
@@ -132,6 +142,15 @@
         mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
     }
 
+    /**
+     * Sets the long click listener for icons
+     */
+    public void setOnIconLongClickListener(OnLongClickListener listener) {
+        for (AdapterHolder holder : mAH) {
+            holder.adapter.setOnIconLongClickListener(listener);
+        }
+    }
+
     public AllAppsStore getAppsStore() {
         return mAllAppsStore;
     }
@@ -140,6 +159,11 @@
         return mMultiValueAlpha.getProperty(index);
     }
 
+    public WorkModeSwitch getWorkModeSwitch() {
+        return mWorkModeSwitch;
+    }
+
+
     @Override
     protected void setDampedScrollShift(float shift) {
         // Bound the shift amount to avoid content from drawing on top (Y-val) of the QSB.
@@ -160,16 +184,23 @@
     }
 
     private void onAppsUpdated() {
-        if (FeatureFlags.ALL_APPS_TABS_ENABLED) {
-            boolean hasWorkApps = false;
-            for (AppInfo app : mAllAppsStore.getApps()) {
-                if (mWorkMatcher.matches(app, null)) {
-                    hasWorkApps = true;
-                    break;
-                }
+        boolean hasWorkApps = false;
+        for (AppInfo app : mAllAppsStore.getApps()) {
+            if (mWorkMatcher.matches(app, null)) {
+                hasWorkApps = true;
+                break;
             }
-            rebindAdapters(hasWorkApps);
         }
+        rebindAdapters(hasWorkApps);
+        if (hasWorkApps) {
+            resetWorkProfile();
+        }
+    }
+
+    private void resetWorkProfile() {
+        mWorkModeSwitch.update(!mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED));
+        mAH[AdapterHolder.WORK].setupOverlay();
+        mAH[AdapterHolder.WORK].applyPadding();
     }
 
     /**
@@ -194,14 +225,6 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-
-        // The AllAppsContainerView houses the QSB and is hence visible from the Workspace
-        // Overview states. We shouldn't intercept for the scrubber in these cases.
-        if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
-            mTouchHandler = null;
-            return false;
-        }
-
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             AllAppsRecyclerView rv = getActiveRecyclerView();
             if (rv != null &&
@@ -221,14 +244,14 @@
     public boolean onTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             AllAppsRecyclerView rv = getActiveRecyclerView();
-            if (rv != null && rv.getScrollbar()
-                .isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
+            if (rv != null && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(),
+                    mFastScrollerOffset)) {
                 mTouchHandler = rv.getScrollbar();
             } else {
                 mTouchHandler = null;
+
             }
         }
-
         if (mTouchHandler != null) {
             mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
             return true;
@@ -257,6 +280,10 @@
         }
     }
 
+    public LayoutInflater getLayoutInflater() {
+        return LayoutInflater.from(getContext());
+    }
+
     /**
      * Resets the state of AllApps.
      */
@@ -304,44 +331,42 @@
     }
 
     @Override
-    public void onDropCompleted(View target, DragObject d, boolean success) { }
+    public void onDropCompleted(View target, DragObject d, boolean success) {
+    }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        if (getApps().hasFilter()) {
-            targetParent.containerType = ContainerType.SEARCHRESULT;
-        } else {
-            targetParent.containerType = ContainerType.ALLAPPS;
-        }
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        parents.add(newContainerTarget(
+                getApps().hasFilter() ? ContainerType.SEARCHRESULT : ContainerType.ALLAPPS));
     }
 
     @Override
     public void setInsets(Rect insets) {
+        mInsets.set(insets);
         DeviceProfile grid = mLauncher.getDeviceProfile();
         int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
                 + grid.cellLayoutPaddingLeftRightPx;
 
         for (int i = 0; i < mAH.length; i++) {
-            mAH[i].adapter.setAppsPerRow(grid.inv.numAllAppsColumns);
             mAH[i].padding.bottom = insets.bottom;
             mAH[i].padding.left = mAH[i].padding.right = leftRightPadding;
             mAH[i].applyPadding();
+            mAH[i].setupOverlay();
         }
 
         ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
-        if (grid.isVerticalBarLayout()) {
-            mlp.leftMargin = insets.left;
-            mlp.rightMargin = insets.right;
-            setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
-        } else {
-            mlp.leftMargin = mlp.rightMargin = 0;
-            setPadding(0, 0, 0, 0);
-        }
+        mlp.leftMargin = insets.left;
+        mlp.rightMargin = insets.right;
         setLayoutParams(mlp);
 
+        if (grid.isVerticalBarLayout()) {
+            setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
+        } else {
+            setPadding(0, 0, 0, 0);
+        }
+
         InsettableFrameLayout.dispatchInsets(this, insets);
-        mLauncher.getAllAppsController()
-                .setScrollRangeDelta(mSearchUiManager.getScrollRangeDelta(insets));
     }
 
     @Override
@@ -374,7 +399,7 @@
         rebindAdapters(showTabs, false /* force */);
     }
 
-    private void rebindAdapters(boolean showTabs, boolean force) {
+    protected void rebindAdapters(boolean showTabs, boolean force) {
         if (showTabs == mUsingTabs && !force) {
             return;
         }
@@ -385,12 +410,22 @@
         mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView);
 
         if (mUsingTabs) {
+            setupWorkToggle();
             mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
             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));
+            findViewById(R.id.tab_work)
+                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
             onTabChanged(mViewPager.getNextPage());
         } else {
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
             mAH[AdapterHolder.WORK].recyclerView = null;
+            if (mWorkModeSwitch != null) {
+                ((ViewGroup) mWorkModeSwitch.getParent()).removeView(mWorkModeSwitch);
+                mWorkModeSwitch = null;
+            }
         }
         setupHeader();
 
@@ -398,6 +433,25 @@
         mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView);
     }
 
+    private void setupWorkToggle() {
+        if (Utilities.ATLEAST_P) {
+            mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
+                    R.layout.work_mode_switch, this, false);
+            this.addView(mWorkModeSwitch);
+            mWorkModeSwitch.setInsets(mInsets);
+            mWorkModeSwitch.post(() -> mAH[AdapterHolder.WORK].applyPadding());
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        View overlay = mAH[AdapterHolder.WORK].getOverlayView();
+        int v = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? GONE : VISIBLE;
+        overlay.findViewById(R.id.work_apps_paused_title).setVisibility(v);
+        overlay.findViewById(R.id.work_apps_paused_content).setVisibility(v);
+    }
+
     private void replaceRVContainer(boolean showTabs) {
         for (int i = 0; i < mAH.length; i++) {
             if (mAH[i].recyclerView != null) {
@@ -408,7 +462,7 @@
         int index = indexOfChild(oldView);
         removeView(oldView);
         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
-        View newView = LayoutInflater.from(getContext()).inflate(layout, this, false);
+        View newView = getLayoutInflater().inflate(layout, this, false);
         addView(newView, index);
         if (showTabs) {
             mViewPager = (AllAppsPagedView) newView;
@@ -425,18 +479,14 @@
 
     public void onTabChanged(int pos) {
         mHeader.setMainActive(pos == 0);
-        reset(true /* animate */);
         if (mAH[pos].recyclerView != null) {
             mAH[pos].recyclerView.bindFastScrollbar();
-
-            findViewById(R.id.tab_personal)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN));
-            findViewById(R.id.tab_work)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
-
         }
-        if (pos == AdapterHolder.WORK) {
-            BottomUserEducationView.showIfNeeded(mLauncher);
+        reset(true /* animate */);
+        if (mWorkModeSwitch != null) {
+            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK
+                    && mAllAppsStore.hasModelFlag(
+                            FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
         }
     }
 
@@ -533,17 +583,6 @@
         return mHeader != null && mHeader.getVisibility() == View.VISIBLE;
     }
 
-    public void onScrollUpEnd() {
-        highlightWorkTabIfNecessary();
-    }
-
-    void highlightWorkTabIfNecessary() {
-        if (mUsingTabs) {
-            ((PersonalWorkSlidingTabStrip) findViewById(R.id.tabs))
-                    .highlightWorkTabIfNecessary();
-        }
-    }
-
     /**
      * Adds an update listener to {@param animator} that adds springs to the animation.
      */
@@ -557,15 +596,8 @@
                         && valueAnimator.getAnimatedFraction() >= FLING_ANIMATION_THRESHOLD) {
                     int searchViewId = getSearchView().getId();
                     addSpringView(searchViewId);
-
                     finishWithShiftAndVelocity(1, velocity * FLING_VELOCITY_MULTIPLIER,
-                            new DynamicAnimation.OnAnimationEndListener() {
-                                @Override
-                                public void onAnimationEnd(DynamicAnimation animation,
-                                        boolean canceled, float value, float velocity) {
-                                    removeSpringView(searchViewId);
-                                }
-                            });
+                            (anim, canceled, value, velocity) -> removeSpringView(searchViewId));
 
                     shouldSpring = false;
                 }
@@ -583,25 +615,32 @@
         public static final int MAIN = 0;
         public static final int WORK = 1;
 
+        private ItemInfoMatcher mInfoMatcher;
+        private final boolean mIsWork;
         public final AllAppsGridAdapter adapter;
         final LinearLayoutManager layoutManager;
         final AlphabeticalAppsList appsList;
         final Rect padding = new Rect();
         AllAppsRecyclerView recyclerView;
         boolean verticalFadingEdge;
+        private View mOverlay;
+
+        boolean mWorkDisabled;
 
         AdapterHolder(boolean isWork) {
+            mIsWork = isWork;
             appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork);
-            adapter = new AllAppsGridAdapter(mLauncher, appsList);
+            adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList);
             appsList.setAdapter(adapter);
             layoutManager = adapter.getLayoutManager();
         }
 
         void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) {
+            mInfoMatcher = matcher;
             appsList.updateItemFilter(matcher);
             recyclerView = (AllAppsRecyclerView) rv;
             recyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
-            recyclerView.setApps(appsList, mUsingTabs);
+            recyclerView.setApps(appsList);
             recyclerView.setLayoutManager(layoutManager);
             recyclerView.setAdapter(adapter);
             recyclerView.setHasFixedSize(true);
@@ -612,11 +651,45 @@
             adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
             applyVerticalFadingEdgeEnabled(verticalFadingEdge);
             applyPadding();
+            setupOverlay();
+        }
+
+        void setupOverlay() {
+            if (!mIsWork || recyclerView == null) return;
+            boolean workDisabled = mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
+            if (mWorkDisabled == workDisabled) return;
+            recyclerView.setContentDescription(workDisabled ? mLauncher.getString(
+                    R.string.work_apps_paused_content_description) : null);
+            View overlayView = getOverlayView();
+            recyclerView.setItemAnimator(new DefaultItemAnimator());
+            if (workDisabled) {
+                overlayView.setAlpha(0);
+                recyclerView.addAutoSizedOverlay(overlayView);
+                overlayView.animate().alpha(1).withEndAction(
+                        () -> {
+                            appsList.updateItemFilter((info, cn) -> false);
+                            recyclerView.setItemAnimator(null);
+                        }).start();
+            } else if (mInfoMatcher != null) {
+                appsList.updateItemFilter(mInfoMatcher);
+                overlayView.animate().alpha(0).withEndAction(() -> {
+                    recyclerView.setItemAnimator(null);
+                    recyclerView.clearAutoSizedOverlays();
+                }).start();
+            }
+            mWorkDisabled = workDisabled;
         }
 
         void applyPadding() {
             if (recyclerView != null) {
-                recyclerView.setPadding(padding.left, padding.top, padding.right, padding.bottom);
+                Resources res = getResources();
+                int switchH = res.getDimensionPixelSize(R.dimen.work_profile_footer_padding) * 2
+                        + mInsets.bottom + Utilities.calculateTextHeight(
+                        res.getDimension(R.dimen.work_profile_footer_text_size));
+
+                int bottomOffset = mWorkModeSwitch != null && mIsWork ? switchH : 0;
+                recyclerView.setPadding(padding.left, padding.top, padding.right,
+                        padding.bottom + bottomOffset);
             }
         }
 
@@ -625,18 +698,12 @@
             mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
                     && verticalFadingEdge);
         }
-    }
 
-    @Override
-    public boolean performAccessibilityAction(int action, Bundle arguments) {
-        if (AccessibilityManagerCompat.processTestRequest(
-                mLauncher, TestProtocol.GET_SCROLL_MESSAGE, action, arguments,
-                response ->
-                        response.putInt(TestProtocol.SCROLL_Y_FIELD,
-                                getActiveRecyclerView().getCurrentScrollY()))) {
-            return true;
+        private View getOverlayView() {
+            if (mOverlay == null) {
+                mOverlay = mLauncher.getLayoutInflater().inflate(R.layout.work_apps_paused, null);
+            }
+            return mOverlay;
         }
-
-        return super.performAccessibilityAction(action, arguments);
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
index 3ee1293..f97eb28 100644
--- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -15,206 +15,90 @@
  */
 package com.android.launcher3.allapps;
 
-import com.android.launcher3.util.Thunk;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
-import java.util.HashSet;
-import java.util.List;
+import com.android.launcher3.allapps.AlphabeticalAppsList.FastScrollSectionInfo;
 
-import androidx.recyclerview.widget.RecyclerView;
+public class AllAppsFastScrollHelper {
 
-public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
+    private static final int NO_POSITION = -1;
 
-    private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
-    private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
+    private int mTargetFastScrollPosition = NO_POSITION;
 
     private AllAppsRecyclerView mRv;
-    private AlphabeticalAppsList mApps;
+    private ViewHolder mLastSelectedViewHolder;
 
-    // Keeps track of the current and targeted fast scroll section (the section to scroll to after
-    // the initial delay)
-    int mTargetFastScrollPosition = -1;
-    @Thunk String mCurrentFastScrollSection;
-    @Thunk String mTargetFastScrollSection;
-
-    // The settled states affect the delay before the fast scroll animation is applied
-    private boolean mHasFastScrollTouchSettled;
-    private boolean mHasFastScrollTouchSettledAtLeastOnce;
-
-    // Set of all views animated during fast scroll.  We keep track of these ourselves since there
-    // is no way to reset a view once it gets scrapped or recycled without other hacks
-    private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
-
-    // Smooth fast-scroll animation frames
-    @Thunk int mFastScrollFrameIndex;
-    @Thunk final int[] mFastScrollFrames = new int[10];
-
-    /**
-     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
-     * if necessary.
-     */
-    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
-                mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
-                mFastScrollFrameIndex++;
-                mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
-            }
-        }
-    };
-
-    /**
-     * This runnable updates the current fast scroll section to the target fastscroll section.
-     */
-    Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
-        @Override
-        public void run() {
-            // Update to the target section
-            mCurrentFastScrollSection = mTargetFastScrollSection;
-            mHasFastScrollTouchSettled = true;
-            mHasFastScrollTouchSettledAtLeastOnce = true;
-            updateTrackedViewsFastScrollFocusState();
-        }
-    };
-
-    public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
+    public AllAppsFastScrollHelper(AllAppsRecyclerView rv) {
         mRv = rv;
-        mApps = apps;
-    }
-
-    public void onSetAdapter(AllAppsGridAdapter adapter) {
-        adapter.setBindViewCallback(this);
     }
 
     /**
      * Smooth scrolls the recycler view to the given section.
-     *
-     * @return whether the fastscroller can scroll to the new section.
      */
-    public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
-            AlphabeticalAppsList.FastScrollSectionInfo info) {
-        if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
-            mTargetFastScrollPosition = info.fastScrollToItem.position;
-            smoothSnapToPosition(scrollY, availableScrollHeight, info);
-            return true;
+    public void smoothScrollToSection(FastScrollSectionInfo info) {
+        if (mTargetFastScrollPosition == info.fastScrollToItem.position) {
+            return;
         }
-        return false;
-    }
-
-    /**
-     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
-     * ourselves and animating the scroll on the recycler view.
-     */
-    private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
-            AlphabeticalAppsList.FastScrollSectionInfo info) {
-        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
-        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
-
-        trackAllChildViews();
-        if (mHasFastScrollTouchSettled) {
-            // In this case, the user has already settled once (and the fast scroll state has
-            // animated) and they are just fine-tuning their section from the last section, so
-            // we should make it feel fast and update immediately.
-            mCurrentFastScrollSection = info.sectionName;
-            mTargetFastScrollSection = null;
-            updateTrackedViewsFastScrollFocusState();
-        } else {
-            // Otherwise, the user has scrubbed really far, and we don't want to distract the user
-            // with the flashing fast scroll state change animation in addition to the fast scroll
-            // section popup, so reset the views to normal, and wait for the touch to settle again
-            // before animating the fast scroll state.
-            mCurrentFastScrollSection = null;
-            mTargetFastScrollSection = info.sectionName;
-            mHasFastScrollTouchSettled = false;
-            updateTrackedViewsFastScrollFocusState();
-
-            // Delay scrolling to a new section until after some duration.  If the user has been
-            // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
-            // fast scroll to settle so it doesn't feel so long.
-            mRv.postDelayed(mFastScrollToTargetSectionRunnable,
-                    mHasFastScrollTouchSettledAtLeastOnce ?
-                            REPEAT_TOUCH_SETTLING_DURATION :
-                            INITIAL_TOUCH_SETTLING_DURATION);
-        }
-
-        // Calculate the full animation from the current scroll position to the final scroll
-        // position, and then run the animation for the duration.  If we are scrolling to the
-        // first fast scroll section, then just scroll to the top of the list itself.
-        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
-                mApps.getFastScrollerSections();
-        int newPosition = info.fastScrollToItem.position;
-        int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
-                        ? 0
-                        : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
-        int numFrames = mFastScrollFrames.length;
-        int deltaY = newScrollY - scrollY;
-        float ySign = Math.signum(deltaY);
-        int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
-        for (int i = 0; i < numFrames; i++) {
-            // TODO(winsonc): We can interpolate this as well.
-            mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
-            deltaY -= step;
-        }
-        mFastScrollFrameIndex = 0;
-        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+        mTargetFastScrollPosition = info.fastScrollToItem.position;
+        mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition));
     }
 
     public void onFastScrollCompleted() {
-        // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
-        //                runs
-
-        // Stop animating the fast scroll position and state
-        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
-        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
-
-        // Reset the tracking variables
-        mHasFastScrollTouchSettled = false;
-        mHasFastScrollTouchSettledAtLeastOnce = false;
-        mCurrentFastScrollSection = null;
-        mTargetFastScrollSection = null;
-        mTargetFastScrollPosition = -1;
-
-        updateTrackedViewsFastScrollFocusState();
-        mTrackedFastScrollViews.clear();
+        mTargetFastScrollPosition = NO_POSITION;
+        setLastHolderSelected(false);
+        mLastSelectedViewHolder = null;
     }
 
-    @Override
-    public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
-        // Update newly bound views to the current fast scroll state if we are fast scrolling
-        if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
-            mTrackedFastScrollViews.add(holder);
+
+    private void setLastHolderSelected(boolean isSelected) {
+        if (mLastSelectedViewHolder != null) {
+            mLastSelectedViewHolder.itemView.setActivated(isSelected);
+            mLastSelectedViewHolder.setIsRecyclable(!isSelected);
         }
     }
 
-    /**
-     * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
-     */
-    private void trackAllChildViews() {
-        int childCount = mRv.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
-            if (viewHolder != null) {
-                mTrackedFastScrollViews.add(viewHolder);
-            }
-        }
-    }
+    private class MyScroller extends LinearSmoothScroller {
 
-    /**
-     * Updates the fast scroll focus on all the children.
-     */
-    private void updateTrackedViewsFastScrollFocusState() {
-        for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
-            int pos = viewHolder.getAdapterPosition();
-            boolean isActive = false;
-            if (mCurrentFastScrollSection != null
-                    && pos > RecyclerView.NO_POSITION
-                    && pos < mApps.getAdapterItems().size()) {
-                AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
-                isActive = item != null &&
-                        mCurrentFastScrollSection.equals(item.sectionName) &&
-                        item.position == mTargetFastScrollPosition;
+        private final int mTargetPosition;
+
+        public MyScroller(int targetPosition) {
+            super(mRv.getContext());
+
+            mTargetPosition = targetPosition;
+            setTargetPosition(targetPosition);
+        }
+
+        @Override
+        protected int getVerticalSnapPreference() {
+            return SNAP_TO_START;
+        }
+
+        @Override
+        protected void onStop() {
+            super.onStop();
+            if (mTargetPosition != mTargetFastScrollPosition) {
+                // Target changed, before the last scroll can finish
+                return;
             }
-            viewHolder.itemView.setActivated(isActive);
+
+            ViewHolder currentHolder = mRv.findViewHolderForAdapterPosition(mTargetPosition);
+            if (currentHolder == mLastSelectedViewHolder) {
+                return;
+            }
+
+            setLastHolderSelected(false);
+            mLastSelectedViewHolder = currentHolder;
+            setLastHolderSelected(true);
+        }
+
+        @Override
+        protected void onStart() {
+            super.onStart();
+            if (mTargetPosition != mTargetFastScrollPosition) {
+                setLastHolderSelected(false);
+                mLastSelectedViewHolder = null;
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index bb21268..8ec4d27 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -15,36 +15,38 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.touch.ItemLongClickListener.INSTANCE_ALL_APPS;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.View.OnFocusChangeListener;
+import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.util.PackageManagerHelper;
-
-import java.util.List;
-
+import androidx.annotation.Nullable;
 import androidx.core.view.accessibility.AccessibilityEventCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.core.view.accessibility.AccessibilityRecordCompat;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.util.PackageManagerHelper;
+
+import java.util.List;
+
 /**
  * The grid view adapter of all the apps.
  */
@@ -64,17 +66,11 @@
 
     // A divider that separates the apps list and the search market button
     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
-    public static final int VIEW_TYPE_WORK_TAB_FOOTER = 1 << 5;
 
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
 
-
-    public interface BindViewCallback {
-        void onBindView(ViewHolder holder);
-    }
-
     /**
      * ViewHolder for each icon.
      */
@@ -174,23 +170,26 @@
         }
     }
 
-    private final Launcher mLauncher;
+    private final BaseDraggingActivity mLauncher;
     private final LayoutInflater mLayoutInflater;
     private final AlphabeticalAppsList mApps;
     private final GridLayoutManager mGridLayoutMgr;
     private final GridSpanSizer mGridSizer;
 
+    private final OnClickListener mOnIconClickListener;
+    private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS;
+
     private int mAppsPerRow;
 
-    private BindViewCallback mBindViewCallback;
     private OnFocusChangeListener mIconFocusListener;
 
     // The text to show when there are no search results and no market search handler.
-    private String mEmptySearchMessage;
+    protected String mEmptySearchMessage;
     // The intent to send off to the market app, updated each time the search query changes.
     private Intent mMarketSearchIntent;
 
-    public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps) {
+    public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
+            AlphabeticalAppsList apps) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -198,7 +197,9 @@
         mGridSizer = new GridSpanSizer();
         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
-        mLayoutInflater = LayoutInflater.from(launcher);
+        mLayoutInflater = inflater;
+
+        mOnIconClickListener = launcher.getItemOnClickListener();
 
         setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
     }
@@ -208,6 +209,13 @@
         mGridLayoutMgr.setSpanCount(mAppsPerRow);
     }
 
+    /**
+     * Sets the long click listener for icons
+     */
+    public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) {
+        mOnIconLongClickListener = listener;
+    }
+
     public static boolean isDividerViewType(int viewType) {
         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
     }
@@ -235,13 +243,6 @@
     }
 
     /**
-     * Sets the callback for when views are bound.
-     */
-    public void setBindViewCallback(BindViewCallback cb) {
-        mBindViewCallback = cb;
-    }
-
-    /**
      * Returns the grid layout manager.
      */
     public GridLayoutManager getLayoutManager() {
@@ -254,8 +255,8 @@
             case VIEW_TYPE_ICON:
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         R.layout.all_apps_icon, parent, false);
-                icon.setOnClickListener(ItemClickHandler.INSTANCE);
-                icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
+                icon.setOnClickListener(mOnIconClickListener);
+                icon.setOnLongClickListener(mOnIconLongClickListener);
                 icon.setLongPressTimeoutFactor(1f);
                 icon.setOnFocusChangeListener(mIconFocusListener);
 
@@ -274,9 +275,6 @@
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.all_apps_divider, parent, false));
-            case VIEW_TYPE_WORK_TAB_FOOTER:
-                View footer = mLayoutInflater.inflate(R.layout.work_tab_footer, parent, false);
-                return new ViewHolder(footer);
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -308,18 +306,6 @@
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 // nothing to do
                 break;
-            case VIEW_TYPE_WORK_TAB_FOOTER:
-                WorkModeSwitch workModeToggle = holder.itemView.findViewById(R.id.work_mode_toggle);
-                workModeToggle.refresh();
-                TextView managedByLabel = holder.itemView.findViewById(R.id.managed_by_label);
-                boolean anyProfileQuietModeEnabled = UserManagerCompat.getInstance(
-                        managedByLabel.getContext()).isAnyProfileQuietModeEnabled();
-                managedByLabel.setText(anyProfileQuietModeEnabled
-                        ? R.string.work_mode_off_label : R.string.work_mode_on_label);
-                break;
-        }
-        if (mBindViewCallback != null) {
-            mBindViewCallback.onBindView(holder);
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index 5b73940..f640c3e 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -48,7 +48,7 @@
     @Override
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
         super.onScrollChanged(l, t, oldl, oldt);
-        mPageIndicator.setScroll(l, mMaxScrollX);
+        mPageIndicator.setScroll(l, mMaxScroll);
     }
 
     @Override
@@ -83,7 +83,7 @@
     }
 
     @Override
-    protected boolean isVerticalScrollable() {
-        return false;
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index f82e380..cbf02b7 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,7 +15,11 @@
  */
 package com.android.launcher3.allapps;
 
+import static android.view.View.MeasureSpec.EXACTLY;
 import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -28,19 +32,19 @@
 
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -49,17 +53,19 @@
 public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider {
 
     private AlphabeticalAppsList mApps;
-    private AllAppsFastScrollHelper mFastScrollHelper;
     private final int mNumAppsPerRow;
 
     // The specific view heights that we use to calculate scroll
-    private SparseIntArray mViewHeights = new SparseIntArray();
-    private SparseIntArray mCachedScrollPositions = new SparseIntArray();
+    private final SparseIntArray mViewHeights = new SparseIntArray();
+    private final SparseIntArray mCachedScrollPositions = new SparseIntArray();
+    private final AllAppsFastScrollHelper mFastScrollHelper;
 
     // The empty-search result background
     private AllAppsBackgroundDrawable mEmptySearchBackground;
     private int mEmptySearchBackgroundTopOffset;
 
+    private ArrayList<View> mAutoSizedOverlays = new ArrayList<>();
+
     public AllAppsRecyclerView(Context context) {
         this(context, null);
     }
@@ -79,14 +85,14 @@
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
         mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+        mFastScrollHelper = new AllAppsFastScrollHelper(this);
     }
 
     /**
      * Sets the list of apps in this view, used to determine the fastscroll position.
      */
-    public void setApps(AlphabeticalAppsList apps, boolean usingTabs) {
+    public void setApps(AlphabeticalAppsList apps) {
         mApps = apps;
-        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
     public AlphabeticalAppsList getApps() {
@@ -94,7 +100,7 @@
     }
 
     private void updatePoolSize() {
-        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
+        DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile();
         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
@@ -143,15 +149,37 @@
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         updateEmptySearchBackgroundBounds();
         updatePoolSize();
+        for (int i = 0; i < mAutoSizedOverlays.size(); i++) {
+            View overlay = mAutoSizedOverlays.get(i);
+            overlay.measure(makeMeasureSpec(w, EXACTLY), makeMeasureSpec(w, EXACTLY));
+            overlay.layout(0, 0, w, h);
+        }
+    }
+
+    /**
+     * Adds an overlay that automatically rescales with the recyclerview.
+     */
+    public void addAutoSizedOverlay(View overlay) {
+        mAutoSizedOverlays.add(overlay);
+        getOverlay().add(overlay);
+        onSizeChanged(getWidth(), getHeight(), getWidth(), getHeight());
+    }
+
+    /**
+     * Clears auto scaling overlay views added by #addAutoSizedOverlay
+     */
+    public void clearAutoSizedOverlays() {
+        for (View v : mAutoSizedOverlays) {
+            getOverlay().remove(v);
+        }
+        mAutoSizedOverlays.clear();
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        if (mApps.hasFilter()) {
-            targetParent.containerType = ContainerType.SEARCHRESULT;
-        } else {
-            targetParent.containerType = ContainerType.ALLAPPS;
-        }
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        parents.add(newContainerTarget(
+                getApps().hasFilter() ? ContainerType.SEARCHRESULT : ContainerType.ALLAPPS));
     }
 
     public void onSearchResultsChanged() {
@@ -193,9 +221,6 @@
             return "";
         }
 
-        // Stop the scroller if it is scrolling
-        stopScroll();
-
         // Find the fastscroll section that maps to this touch fraction
         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                 mApps.getFastScrollerSections();
@@ -208,10 +233,7 @@
             lastInfo = info;
         }
 
-        // Update the fast scroll
-        int scrollY = getCurrentScrollY();
-        int availableScrollHeight = getAvailableScrollHeight();
-        mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
+        mFastScrollHelper.smoothScrollToSection(lastInfo);
         return lastInfo.sectionName;
     }
 
@@ -229,7 +251,6 @@
                 mCachedScrollPositions.clear();
             }
         });
-        mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index c4b2f68..3ae0a18 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -15,22 +15,23 @@
  */
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.AppInfo.COMPONENT_KEY_COMPARATOR;
-import static com.android.launcher3.AppInfo.EMPTY_ARRAY;
+import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
+import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
 
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.PromiseAppInfo;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PromiseAppInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -49,8 +50,9 @@
 
     private AppInfo[] mApps = EMPTY_ARRAY;
 
-    private final List<OnUpdateListener> mUpdateListeners = new ArrayList<>();
+    private final List<OnUpdateListener> mUpdateListeners = new CopyOnWriteArrayList<>();
     private final ArrayList<ViewGroup> mIconContainers = new ArrayList<>();
+    private int mModelFlags;
 
     private int mDeferUpdatesFlags = 0;
     private boolean mUpdatePending = false;
@@ -62,11 +64,21 @@
     /**
      * Sets the current set of apps.
      */
-    public void setApps(AppInfo[] apps) {
+    public void setApps(AppInfo[] apps, int flags) {
         mApps = apps;
+        mModelFlags = flags;
         notifyUpdate();
     }
 
+    /**
+     * @see com.android.launcher3.model.BgDataModel.Callbacks#FLAG_QUIET_MODE_ENABLED
+     * @see com.android.launcher3.model.BgDataModel.Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
+     * @see com.android.launcher3.model.BgDataModel.Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
+     */
+    public boolean hasModelFlag(int mask) {
+        return (mModelFlags & mask) != 0;
+    }
+
     public AppInfo getApp(ComponentKey key) {
         mTempInfo.componentName = key.componentName;
         mTempInfo.user = key.user;
@@ -99,9 +111,8 @@
             mUpdatePending = true;
             return;
         }
-        int count = mUpdateListeners.size();
-        for (int i = 0; i < count; i++) {
-            mUpdateListeners.get(i).onAppsUpdated();
+        for (OnUpdateListener listener : mUpdateListeners) {
+            listener.onAppsUpdated();
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 08ce9c2..a9b030e 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -2,35 +2,43 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
+import static com.android.launcher3.LauncherState.APPS_VIEW_ITEM_MASK;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
 import android.util.FloatProperty;
+import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.Interpolator;
+import android.widget.EditText;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.anim.SpringObjectAnimator;
-import com.android.launcher3.util.Themes;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.views.ScrimView;
+import com.android.systemui.plugins.AllAppsSearchPlugin;
+import com.android.systemui.plugins.PluginListener;
 
 /**
  * Handles AllApps view transition.
@@ -42,10 +50,8 @@
  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
  * closer to top or closer to the page indicator.
  */
-public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener {
-
-    private static final float SPRING_DAMPING_RATIO = 0.9f;
-    private static final float SPRING_STIFFNESS = 600f;
+public class AllAppsTransitionController implements StateHandler<LauncherState>,
+        OnDeviceProfileChangeListener, PluginListener<AllAppsSearchPlugin> {
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -67,7 +73,6 @@
     private ScrimView mScrimView;
 
     private final Launcher mLauncher;
-    private final boolean mIsDarkTheme;
     private boolean mIsVerticalLayout;
 
     // Animation in this class is controlled by a single variable {@link mProgress}.
@@ -81,12 +86,15 @@
 
     private float mScrollRangeDelta = 0;
 
+    // plugin related variables
+    private AllAppsSearchPlugin mPlugin;
+    private View mPluginContent;
+
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
         mShiftRange = mLauncher.getDeviceProfile().heightPx;
         mProgress = 1f;
 
-        mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
         mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout();
         mLauncher.addOnDeviceProfileChangeListener(this);
     }
@@ -114,7 +122,7 @@
      * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
      *
      * @see #setState(LauncherState)
-     * @see #setStateWithAnimation(LauncherState, AnimatorSetBuilder, AnimationConfig)
+     * @see #setStateWithAnimation(LauncherState, StateAnimationConfig, PendingAnimation)
      */
     public void setProgress(float progress) {
         mProgress = progress;
@@ -122,15 +130,8 @@
         float shiftCurrent = progress * mShiftRange;
 
         mAppsView.setTranslationY(shiftCurrent);
-
-        // Use a light system UI (dark icons) if all apps is behind at least half of the
-        // status bar.
-        boolean forceChange = shiftCurrent - mScrimView.getDragHandleSize()
-                <= mLauncher.getDeviceProfile().getInsets().top / 2;
-        if (forceChange) {
-            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, !mIsDarkTheme);
-        } else {
-            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0);
+        if (mPlugin != null) {
+            mPlugin.setProgress(progress);
         }
     }
 
@@ -145,7 +146,7 @@
     @Override
     public void setState(LauncherState state) {
         setProgress(state.getVerticalProgress(mLauncher));
-        setAlphas(state, null, new AnimatorSetBuilder());
+        setAlphas(state, new StateAnimationConfig(), NO_ANIM_PROPERTY_SETTER);
         onProgressAnimationEnd();
     }
 
@@ -155,73 +156,80 @@
      */
     @Override
     public void setStateWithAnimation(LauncherState toState,
-            AnimatorSetBuilder builder, AnimationConfig config) {
+            StateAnimationConfig config, PendingAnimation builder) {
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
-            setAlphas(toState, config, builder);
+            if (!config.onlyPlayAtomicComponent()) {
+                setAlphas(toState, config, builder);
+            }
             // Fail fast
             onProgressAnimationEnd();
             return;
         }
 
-        if (!config.playNonAtomicComponent()) {
+        if (config.onlyPlayAtomicComponent()) {
             // There is no atomic component for the all apps transition, so just return early.
             return;
         }
 
         Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
-                ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
+                ? config.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
                 : FAST_OUT_SLOW_IN;
-        Animator anim = createSpringAnimation(mProgress, targetProgress);
-        anim.setDuration(config.duration);
-        anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
-        anim.addListener(getProgressAnimatorListener());
 
-        builder.play(anim);
+        Animator anim = createSpringAnimation(mProgress, targetProgress);
+        anim.setInterpolator(config.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
+        anim.addListener(getProgressAnimatorListener());
+        builder.add(anim);
 
         setAlphas(toState, config, builder);
     }
 
     public Animator createSpringAnimation(float... progressValues) {
-        return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
-                SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+        return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
     }
 
-    private void setAlphas(LauncherState toState, AnimationConfig config,
-            AnimatorSetBuilder builder) {
-        setAlphas(toState.getVisibleElements(mLauncher), config, builder);
-    }
-
-    public void setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder) {
-        PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER
-                : config.getPropertySetter(builder);
+    /**
+     * Updates the property for the provided state
+     */
+    public void setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter) {
+        int visibleElements = state.getVisibleElements(mLauncher);
         boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0;
         boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0;
 
-        Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
-        Interpolator headerFade = builder.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
-        setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
-        setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
-        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasAllAppsContent,
-                setter, headerFade, allAppsFade);
+        boolean hasAnyVisibleItem = (visibleElements & APPS_VIEW_ITEM_MASK) != 0;
+
+        Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
+        Interpolator headerFade = config.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
+
+        if (mPlugin == null) {
+            setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
+            setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
+            mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
+                    hasAllAppsContent, setter, headerFade, allAppsFade);
+        } else {
+            setter.setViewAlpha(mPluginContent, hasAllAppsContent ? 1 : 0, allAppsFade);
+            setter.setViewAlpha(mAppsView.getContentView(), 0, allAppsFade);
+            setter.setViewAlpha(mAppsView.getScrollBar(), 0, allAppsFade);
+        }
         mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
         setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
                 (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
+
+        // Set visibility of the container at the very beginning or end of the transition.
+        setter.setViewAlpha(mAppsView, hasAnyVisibleItem ? 1 : 0,
+                hasAnyVisibleItem ? INSTANT : FINAL_FRAME);
     }
 
     public AnimatorListenerAdapter getProgressAnimatorListener() {
-        return new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                onProgressAnimationEnd();
-            }
-        };
+        return AnimationSuccessListener.forRunnable(this::onProgressAnimationEnd);
     }
 
-    public void setupViews(AllAppsContainerView appsView) {
+    public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
         mAppsView = appsView;
-        mScrimView = mLauncher.findViewById(R.id.scrim_view);
+        mScrimView = scrimView;
+        PluginManagerWrapper.INSTANCE.get(mLauncher)
+                .addPluginListener(this, AllAppsSearchPlugin.class, false);
     }
 
     /**
@@ -243,18 +251,48 @@
     private void onProgressAnimationEnd() {
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
-        } else if (isAllAppsExpanded()) {
-            mAppsView.onScrollUpEnd();
         }
+        updatePluginAnimationEnd();
     }
 
-    private boolean isAllAppsExpanded() {
-        return Float.compare(mProgress, 0f) == 0;
+    @Override
+    public void onPluginConnected(AllAppsSearchPlugin plugin, Context context) {
+        mPlugin = plugin;
+        mPluginContent = mLauncher.getLayoutInflater().inflate(
+                R.layout.all_apps_content_layout, mAppsView, false);
+        mAppsView.addView(mPluginContent);
+        mPluginContent.setAlpha(0f);
+        mPlugin.setup((ViewGroup) mPluginContent, mLauncher, mShiftRange);
     }
 
-    public void highlightWorkTabIfNecessary() {
-        if (isAllAppsExpanded()) {
-            mAppsView.highlightWorkTabIfNecessary();
+    @Override
+    public void onPluginDisconnected(AllAppsSearchPlugin plugin) {
+        mPlugin = null;
+        mAppsView.removeView(mPluginContent);
+    }
+
+    public void onActivityDestroyed() {
+        PluginManagerWrapper.INSTANCE.get(mLauncher).removePluginListener(this);
+    }
+
+    /** Used for the plugin to signal when drag starts happens
+     * @param toAllApps*/
+    public void onDragStart(boolean toAllApps) {
+        if (mPlugin == null) return;
+
+        if (toAllApps) {
+            EditText editText = mAppsView.getSearchUiManager().setTextSearchEnabled(true);
+            mPlugin.setEditText(editText);
+        }
+        mPlugin.onDragStart(toAllApps ? 1f : 0f);
+    }
+
+    private void updatePluginAnimationEnd() {
+        if (mPlugin == null) return;
+        mPlugin.onAnimationEnd(mProgress);
+        if (Float.compare(mProgress, 1f) == 0) {
+            mAppsView.getSearchUiManager().setTextSearchEnabled(false);
+            mPlugin.setEditText(null);
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0c4be62..06209bb 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,13 +15,11 @@
  */
 package com.android.launcher3.allapps;
 
-import android.content.Context;
-import android.content.pm.PackageManager;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import android.content.Context;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
@@ -116,16 +114,9 @@
             item.position = pos;
             return item;
         }
-
-        public static AdapterItem asWorkTabFooter(int pos) {
-            AdapterItem item = new AdapterItem();
-            item.viewType = AllAppsGridAdapter.VIEW_TYPE_WORK_TAB_FOOTER;
-            item.position = pos;
-            return item;
-        }
     }
 
-    private final Launcher mLauncher;
+    private final BaseDraggingActivity mLauncher;
 
     // The set of apps from the system
     private final List<AppInfo> mApps = new ArrayList<>();
@@ -150,7 +141,7 @@
 
     public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) {
         mAllAppsStore = appsStore;
-        mLauncher = Launcher.getLauncher(context);
+        mLauncher = BaseDraggingActivity.fromContext(context);
         mAppNameComparator = new AppInfoComparator(context);
         mIsWork = isWork;
         mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
@@ -389,18 +380,6 @@
                     break;
             }
         }
-
-        // Add the work profile footer if required.
-        if (shouldShowWorkFooter()) {
-            mAdapterItems.add(AdapterItem.asWorkTabFooter(position++));
-        }
-    }
-
-    private boolean shouldShowWorkFooter() {
-        return mIsWork && Utilities.ATLEAST_P &&
-                (DeepShortcutManager.getInstance(mLauncher).hasHostPermission()
-                        || mLauncher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
-                        == PackageManager.PERMISSION_GRANTED);
     }
 
     private List<AppInfo> getFiltersAppInfos() {
diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java
index 80577a7..823f98e 100644
--- a/src/com/android/launcher3/allapps/AppInfoComparator.java
+++ b/src/com/android/launcher3/allapps/AppInfoComparator.java
@@ -19,8 +19,8 @@
 import android.os.Process;
 import android.os.UserHandle;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.LabelComparator;
 
 import java.util.Comparator;
@@ -30,12 +30,12 @@
  */
 public class AppInfoComparator implements Comparator<AppInfo> {
 
-    private final UserManagerCompat mUserManager;
+    private final UserCache mUserManager;
     private final UserHandle mMyUser;
     private final LabelComparator mLabelComparator;
 
     public AppInfoComparator(Context context) {
-        mUserManager = UserManagerCompat.getInstance(context);
+        mUserManager = UserCache.INSTANCE.get(context);
         mMyUser = Process.myUserHandle();
         mLabelComparator = new LabelComparator();
     }
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index 8c59626..b4ff5ea 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -24,19 +24,18 @@
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
-import android.content.SharedPreferences;
 import android.os.Handler;
+import android.os.UserManager;
 import android.view.MotionEvent;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.states.InternalStateHandler;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.OnboardingPrefs;
 
 /**
  * Abstract base class of floating view responsible for showing discovery bounce animation
@@ -45,17 +44,10 @@
 
     private static final long DELAY_MS = 450;
 
-    public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
-    public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
-    public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
-    public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
-
-    public static final int BOUNCE_MAX_COUNT = 3;
-
     private final Launcher mLauncher;
     private final Animator mDiscoBounceAnimation;
 
-    private final StateListener mStateListener = new StateListener() {
+    private final StateListener<LauncherState> mStateListener = new StateListener<LauncherState>() {
         @Override
         public void onStateTransitionStart(LauncherState toState) {
             handleClose(false);
@@ -144,11 +136,11 @@
     }
 
     private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) {
+        OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
         if (!launcher.isInState(NORMAL)
-                || (launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)
-                && !shouldShowForWorkProfile(launcher))
+                || onboardingPrefs.getBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN)
                 || AbstractFloatingView.getTopOpenView(launcher) != null
-                || UserManagerCompat.getInstance(launcher).isDemoUser()
+                || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return;
         }
@@ -157,36 +149,39 @@
             new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS);
             return;
         }
-        incrementHomeBounceCount(launcher);
+        onboardingPrefs.incrementEventCount(OnboardingPrefs.HOME_BOUNCE_COUNT);
 
         new DiscoveryBounce(launcher, 0).show(HOTSEAT);
     }
 
-    public static void showForOverviewIfNeeded(Launcher launcher) {
-        showForOverviewIfNeeded(launcher, true);
+    public static void showForOverviewIfNeeded(Launcher launcher,
+                                               PagedOrientationHandler orientationHandler) {
+        showForOverviewIfNeeded(launcher, true, orientationHandler);
     }
 
-    private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay) {
+    private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay,
+                                                PagedOrientationHandler orientationHandler) {
+        OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
         if (!launcher.isInState(OVERVIEW)
                 || !launcher.hasBeenResumed()
                 || launcher.isForceInvisible()
                 || launcher.getDeviceProfile().isVerticalBarLayout()
-                || (launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)
-                && !shouldShowForWorkProfile(launcher))
-                || UserManagerCompat.getInstance(launcher).isDemoUser()
+                || !orientationHandler.isLayoutNaturalToLauncher()
+                || onboardingPrefs.getBoolean(OnboardingPrefs.SHELF_BOUNCE_SEEN)
+                || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return;
         }
 
         if (withDelay) {
-            new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false), DELAY_MS);
+            new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false,
+                    orientationHandler), DELAY_MS);
             return;
-        } else if (InternalStateHandler.hasPending()
-                || AbstractFloatingView.getTopOpenView(launcher) != null) {
+        } else if (AbstractFloatingView.getTopOpenView(launcher) != null) {
             // TODO: Move these checks to the top and call this method after invalidate handler.
             return;
         }
-        incrementShelfBounceCount(launcher);
+        onboardingPrefs.incrementEventCount(OnboardingPrefs.SHELF_BOUNCE_COUNT);
 
         new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher)))
                 .show(PREDICTION);
@@ -213,28 +208,4 @@
             mController.setProgress(progress - mDelta);
         }
     }
-
-    private static boolean shouldShowForWorkProfile(Launcher launcher) {
-        return !launcher.getSharedPrefs().getBoolean(
-                PersonalWorkSlidingTabStrip.KEY_SHOWED_PEEK_WORK_TAB, false)
-                && UserManagerCompat.getInstance(launcher).hasWorkProfile();
-    }
-
-    private static void incrementShelfBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply();
-    }
-
-    private static void incrementHomeBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply();
-    }
 }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 42a0eee..81e1b94 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Point;
@@ -31,9 +33,9 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
@@ -370,7 +372,7 @@
         }
 
         allowTouchForwarding(hasAllAppsContent);
-        setter.setFloat(mTabLayout, ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
+        setter.setFloat(mTabLayout, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
     }
 
     protected void allowTouchForwarding(boolean allow) {
@@ -393,7 +395,7 @@
 
     @Override
     public void setInsets(Rect insets) {
-        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
+        DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile();
         for (FloatingHeaderRow row : mAllRows) {
             row.setInsets(insets, grid);
         }
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
new file mode 100644
index 0000000..a6bc6cf
--- /dev/null
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -0,0 +1,96 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.views.WorkEduView;
+
+/**
+ * AllAppsContainerView with launcher specific callbacks
+ */
+public class LauncherAllAppsContainerView extends AllAppsContainerView {
+
+    private final Launcher mLauncher;
+
+    private StateListener<LauncherState> mWorkTabListener;
+
+    public LauncherAllAppsContainerView(Context context) {
+        this(context, null);
+    }
+
+    public LauncherAllAppsContainerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public LauncherAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        // The AllAppsContainerView houses the QSB and is hence visible from the Workspace
+        // Overview states. We shouldn't intercept for the scrubber in these cases.
+        if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
+            mTouchHandler = null;
+            return false;
+        }
+
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
+            return false;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        super.setInsets(insets);
+        mLauncher.getAllAppsController()
+                .setScrollRangeDelta(mSearchUiManager.getScrollRangeDelta(insets));
+    }
+
+    @Override
+    public void setupHeader() {
+        super.setupHeader();
+        if (mWorkTabListener != null && !mUsingTabs) {
+            mLauncher.getStateManager().removeStateListener(mWorkTabListener);
+        }
+    }
+
+    @Override
+    public void onTabChanged(int pos) {
+        super.onTabChanged(pos);
+        if (mUsingTabs) {
+            if (pos == AdapterHolder.WORK) {
+                WorkEduView.showWorkEduIfNeeded(mLauncher);
+            } else {
+                mWorkTabListener = WorkEduView.showEduFlowIfNeeded(mLauncher, mWorkTabListener);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
index decdcc0..2de425e 100644
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.allapps;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.util.AttributeSet;
@@ -24,15 +23,14 @@
 import android.widget.Button;
 import android.widget.LinearLayout;
 
-import com.android.launcher3.Launcher;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.util.Themes;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * Supports two indicator colors, dedicated for personal and work tabs.
  */
@@ -40,11 +38,8 @@
     private static final int POSITION_PERSONAL = 0;
     private static final int POSITION_WORK = 1;
 
-    public static final String KEY_SHOWED_PEEK_WORK_TAB = "showed_peek_work_tab";
-
     private final Paint mSelectedIndicatorPaint;
     private final Paint mDividerPaint;
-    private final SharedPreferences mSharedPreferences;
 
     private int mSelectedIndicatorHeight;
     private int mIndicatorLeft = -1;
@@ -73,16 +68,13 @@
         mDividerPaint.setStrokeWidth(
                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
 
-        mSharedPreferences = Launcher.getLauncher(getContext()).getSharedPrefs();
         mIsRtl = Utilities.isRtl(getResources());
     }
 
-    private void updateIndicatorPosition(float scrollOffset) {
-        mScrollOffset = scrollOffset;
-        updateIndicatorPosition();
-    }
-
-    private void updateTabTextColor(int pos) {
+    /**
+     * Highlights tab with index pos
+     */
+    public void updateTabTextColor(int pos) {
         mSelectedPosition = pos;
         for (int i = 0; i < getChildCount(); i++) {
             Button tab = (Button) getChildAt(i);
@@ -90,6 +82,11 @@
         }
     }
 
+    private void updateIndicatorPosition(float scrollOffset) {
+        mScrollOffset = scrollOffset;
+        updateIndicatorPosition();
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
@@ -129,25 +126,6 @@
             mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
     }
 
-    public void highlightWorkTabIfNecessary() {
-        if (mSharedPreferences.getBoolean(KEY_SHOWED_PEEK_WORK_TAB, false)) {
-            return;
-        }
-        if (mLastActivePage != POSITION_PERSONAL) {
-            return;
-        }
-        highlightWorkTab();
-        mSharedPreferences.edit().putBoolean(KEY_SHOWED_PEEK_WORK_TAB, true).apply();
-    }
-
-    private void highlightWorkTab() {
-        View v = getChildAt(POSITION_WORK);
-        v.post(() -> {
-            v.setPressed(true);
-            v.setPressed(false);
-        });
-    }
-
     @Override
     public void setScroll(int currentScroll, int totalScroll) {
         float scrollOffset = ((float) currentScroll) / totalScroll;
@@ -158,6 +136,7 @@
     public void setActiveMarker(int activePage) {
         updateTabTextColor(activePage);
         if (mContainerView != null && mLastActivePage != activePage) {
+            updateIndicatorPosition(activePage);
             mContainerView.onTabChanged(activePage);
         }
         mLastActivePage = activePage;
diff --git a/src/com/android/launcher3/allapps/PluginHeaderRow.java b/src/com/android/launcher3/allapps/PluginHeaderRow.java
index 535ef54..3089b18 100644
--- a/src/com/android/launcher3/allapps/PluginHeaderRow.java
+++ b/src/com/android/launcher3/allapps/PluginHeaderRow.java
@@ -15,10 +15,11 @@
  */
 package com.android.launcher3.allapps;
 
-import static android.view.View.ALPHA;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+
 import android.graphics.Rect;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -67,7 +68,7 @@
     public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
             PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
         // Don't use setViewAlpha as we want to control the visibility ourselves.
-        setter.setFloat(mView, ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
+        setter.setFloat(mView, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index cf9a088..7d5363f 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -15,9 +15,14 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
+
 import android.graphics.Rect;
 import android.view.KeyEvent;
 import android.view.animation.Interpolator;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.anim.PropertySetter;
 
@@ -52,4 +57,21 @@
      */
     void setContentVisibility(int visibleElements, PropertySetter setter,
             Interpolator interpolator);
+
+    /**
+     * Returns true if the QSB should be visible for the given set of visible elements
+     */
+    default boolean isQsbVisible(int visibleElements) {
+        return (visibleElements & ALL_APPS_HEADER) != 0;
+    }
+
+    /**
+     * Called to control how the search UI result should be handled.
+     *
+     * @param isEnabled when {@code true}, the search is all handled inside AOSP
+     *                  and is not overlayable.
+     * @return the searchbox edit text object
+     */
+    @Nullable
+    EditText setTextSearchEnabled(boolean isEnabled);
 }
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 717bbd4..4567ee6 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,61 +16,124 @@
 package com.android.launcher3.allapps;
 
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Rect;
 import android.os.AsyncTask;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 import android.widget.Switch;
 
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.views.ArrowTipView;
 
 import java.lang.ref.WeakReference;
-import java.util.List;
 
-public class WorkModeSwitch extends Switch {
+/**
+ * Work profile toggle switch shown at the bottom of AllApps work tab
+ */
+public class WorkModeSwitch extends Switch implements Insettable {
+
+    private static final int WORK_TIP_THRESHOLD = 2;
+    public static final String KEY_WORK_TIP_COUNTER = "worked_tip_counter";
+
+    private Rect mInsets = new Rect();
+
+    private final float[] mTouch = new float[2];
+    private int mTouchSlop;
 
     public WorkModeSwitch(Context context) {
         super(context);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
+        mTouchSlop = viewConfiguration.getScaledTouchSlop();
     }
 
     @Override
-    public void setChecked(boolean checked) {
-        // No-op, do not change the checked state until broadcast is received.
-    }
+    public void setChecked(boolean checked) { }
 
     @Override
     public void toggle() {
+        // don't show tip if user uses toggle
+        Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
         trySetQuietModeEnabledToAllProfilesAsync(isChecked());
     }
 
-    private void setCheckedInternal(boolean checked) {
-        super.setChecked(checked);
-    }
-
-    public void refresh() {
-        UserManagerCompat userManager = UserManagerCompat.getInstance(getContext());
-        setCheckedInternal(!userManager.isAnyProfileQuietModeEnabled());
+    /**
+     * Sets the enabled or disabled state of the button
+     * @param isChecked
+     */
+    public void update(boolean isChecked) {
+        super.setChecked(isChecked);
+        setCompoundDrawablesRelativeWithIntrinsicBounds(
+                isChecked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
         setEnabled(true);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        return ev.getActionMasked() == MotionEvent.ACTION_MOVE || super.onTouchEvent(ev);
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mTouch[0] = ev.getX();
+            mTouch[1] = ev.getY();
+        } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
+            if (Math.abs(mTouch[0] - ev.getX()) > mTouchSlop
+                    || Math.abs(mTouch[1] - ev.getY()) > mTouchSlop) {
+                int action = ev.getAction();
+                ev.setAction(MotionEvent.ACTION_CANCEL);
+                super.onTouchEvent(ev);
+                ev.setAction(action);
+                return false;
+            }
+        }
+        return super.onTouchEvent(ev);
     }
 
     private void trySetQuietModeEnabledToAllProfilesAsync(boolean enabled) {
         new SetQuietModeEnabledAsyncTask(enabled, new WeakReference<>(this)).execute();
     }
 
+    @Override
+    public void setInsets(Rect insets) {
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+                getPaddingBottom() + bottomInset);
+    }
+
+    /**
+     * Animates in/out work profile toggle panel based on the tab user is on
+     */
+    public void setWorkTabVisible(boolean workTabVisible) {
+        clearAnimation();
+        if (workTabVisible) {
+            setVisibility(VISIBLE);
+            setAlpha(0);
+            animate().alpha(1).start();
+            showTipIfNeeded();
+        } else {
+            animate().alpha(0).withEndAction(() -> this.setVisibility(GONE)).start();
+        }
+    }
+
     private static final class SetQuietModeEnabledAsyncTask
             extends AsyncTask<Void, Void, Boolean> {
 
@@ -95,14 +158,14 @@
         @Override
         protected Boolean doInBackground(Void... voids) {
             WorkModeSwitch workModeSwitch = switchWeakReference.get();
-            if (workModeSwitch == null) {
+            if (workModeSwitch == null || !Utilities.ATLEAST_P) {
                 return false;
             }
-            UserManagerCompat userManager =
-                    UserManagerCompat.getInstance(workModeSwitch.getContext());
-            List<UserHandle> userProfiles = userManager.getUserProfiles();
+
+            Context context = workModeSwitch.getContext();
+            UserManager userManager = context.getSystemService(UserManager.class);
             boolean showConfirm = false;
-            for (UserHandle userProfile : userProfiles) {
+            for (UserHandle userProfile : UserCache.INSTANCE.get(context).getUserProfiles()) {
                 if (Process.myUserHandle().equals(userProfile)) {
                     continue;
                 }
@@ -121,4 +184,19 @@
             }
         }
     }
+
+    /**
+     * Shows a work tip on the Nth work tab open
+     */
+    public void showTipIfNeeded() {
+        Context context = getContext();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        int tipCounter = prefs.getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
+        if (tipCounter < 0) return;
+        if (tipCounter == 0) {
+            new ArrowTipView(context)
+                    .show(context.getString(R.string.work_switch_tip), getTop());
+        }
+        prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
+    }
 }
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 4515dde..ed45749 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -25,8 +25,8 @@
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.util.ComponentKey;
@@ -41,7 +41,7 @@
         implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener,
         OnFocusChangeListener {
 
-    protected Launcher mLauncher;
+    protected BaseDraggingActivity mLauncher;
     protected Callbacks mCb;
     protected ExtendedEditText mInput;
     protected String mQuery;
@@ -56,7 +56,7 @@
      */
     public final void initialize(
             SearchAlgorithm searchAlgorithm, ExtendedEditText input,
-            Launcher launcher, Callbacks cb) {
+            BaseDraggingActivity launcher, Callbacks cb) {
         mCb = cb;
         mLauncher = launcher;
 
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 31fcc8c..356c52c 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -19,15 +19,12 @@
 import static android.view.View.MeasureSpec.getSize;
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
 import static com.android.launcher3.Utilities.prefixTextWithIcon;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.text.Selection;
-import android.text.Spannable;
-import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
@@ -35,19 +32,18 @@
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
 import android.view.animation.Interpolator;
+import android.widget.EditText;
 
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.util.ComponentKey;
 
 import java.util.ArrayList;
@@ -59,17 +55,15 @@
         implements SearchUiManager, AllAppsSearchBarController.Callbacks,
         AllAppsStore.OnUpdateListener, Insettable {
 
-
-    private final Launcher mLauncher;
+    private final BaseDraggingActivity mLauncher;
     private final AllAppsSearchBarController mSearchBarController;
     private final SpannableStringBuilder mSearchQueryBuilder;
 
     private AlphabeticalAppsList mApps;
     private AllAppsContainerView mAppsView;
 
-    // This value was used to position the QSB. We store it here for translationY animations.
-    private final float mFixedTranslationY;
-    private final float mMarginTopAdjusting;
+    // The amount of pixels to shift down and overlap with the rest of the content.
+    private final int mContentOverlap;
 
     public AppsSearchContainerLayout(Context context) {
         this(context, null);
@@ -82,28 +76,27 @@
     public AppsSearchContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
-        mLauncher = Launcher.getLauncher(context);
+        mLauncher = BaseDraggingActivity.fromContext(context);
         mSearchBarController = new AllAppsSearchBarController();
 
         mSearchQueryBuilder = new SpannableStringBuilder();
         Selection.setSelection(mSearchQueryBuilder, 0);
-
-        mFixedTranslationY = getTranslationY();
-        mMarginTopAdjusting = mFixedTranslationY - getPaddingTop();
-
         setHint(prefixTextWithIcon(getContext(), R.drawable.ic_allapps_search, getHint()));
+
+        mContentOverlap =
+                getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_field_height) / 2;
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mLauncher.getAppsView().getAppsStore().addUpdateListener(this);
+        mAppsView.getAppsStore().addUpdateListener(this);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mLauncher.getAppsView().getAppsStore().removeUpdateListener(this);
+        mAppsView.getAppsStore().removeUpdateListener(this);
     }
 
     @Override
@@ -133,6 +126,8 @@
         int expectedLeft = parent.getPaddingLeft() + (availableWidth - myWidth) / 2;
         int shift = expectedLeft - left;
         setTranslationX(shift);
+
+        offsetTopAndBottom(mContentOverlap);
     }
 
     @Override
@@ -201,7 +196,7 @@
     @Override
     public void setInsets(Rect insets) {
         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
-        mlp.topMargin = Math.round(Math.max(-mFixedTranslationY, insets.top - mMarginTopAdjusting));
+        mlp.topMargin = insets.top;
         requestLayout();
     }
 
@@ -210,15 +205,18 @@
         if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
             return 0;
         } else {
-            int topMargin = Math.round(Math.max(
-                    -mFixedTranslationY, insets.top - mMarginTopAdjusting));
-           return insets.bottom + topMargin + mFixedTranslationY;
+            return insets.bottom + insets.top;
         }
     }
 
     @Override
     public void setContentVisibility(int visibleElements, PropertySetter setter,
             Interpolator interpolator) {
-        setter.setViewAlpha(this, (visibleElements & ALL_APPS_HEADER) != 0 ? 1 : 0, interpolator);
+        setter.setViewAlpha(this, isQsbVisible(visibleElements) ? 1 : 0, interpolator);
+    }
+
+    @Override
+    public EditText setTextSearchEnabled(boolean isEnabled) {
+        return this;
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 26f6ec3..f72a988 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -17,7 +17,7 @@
 
 import android.os.Handler;
 
-import com.android.launcher3.AppInfo;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.ComponentKey;
 
 import java.text.Collator;
diff --git a/src/com/android/launcher3/anim/AnimationSuccessListener.java b/src/com/android/launcher3/anim/AnimationSuccessListener.java
index 9448632..9905e81 100644
--- a/src/com/android/launcher3/anim/AnimationSuccessListener.java
+++ b/src/com/android/launcher3/anim/AnimationSuccessListener.java
@@ -39,4 +39,25 @@
     }
 
     public abstract void onAnimationSuccess(Animator animator);
+
+    /**
+     * Returns an AnimationSuccessListener which runs the provided action on success
+     */
+    public static AnimationSuccessListener forRunnable(Runnable r) {
+        return new RunnableSuccessListener(r);
+    }
+
+    private static class RunnableSuccessListener extends AnimationSuccessListener {
+
+        private final Runnable mRunnable;
+
+        private RunnableSuccessListener(Runnable r) {
+            mRunnable = r;
+        }
+
+        @Override
+        public void onAnimationSuccess(Animator animator) {
+            mRunnable.run();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 4a52795..ea0ff8b 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -15,8 +15,11 @@
  */
 package com.android.launcher3.anim;
 
+import static com.android.launcher3.Utilities.boundToRange;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
@@ -24,16 +27,15 @@
 import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
-import android.util.Log;
+import android.content.Context;
 
-import androidx.dynamicanimation.animation.DynamicAnimation;
-import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.annotation.Nullable;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 /**
  * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators
@@ -42,14 +44,7 @@
  * Note: The implementation does not support start delays on child animations or
  * sequential playbacks.
  */
-public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
-
-    private static final String TAG = "AnimatorPlaybackCtrler";
-    private static boolean DEBUG = false;
-
-    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
-        return wrap(anim, duration, null);
-    }
+public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
     /**
      * Creates an animation controller for the provided animation.
@@ -57,20 +52,21 @@
      * needs to be larger than the total number of pixels so that we don't have jittering due
      * to float (animation-fraction * total duration) to int conversion.
      */
-    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration,
-            Runnable onCancelRunnable) {
+    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
+        ArrayList<Holder> childAnims = new ArrayList<>();
+        addAnimationHoldersRecur(anim, duration, SpringProperty.DEFAULT, childAnims);
 
-        /**
-         * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
-         */
-        return new AnimatorPlaybackControllerVL(anim, duration, onCancelRunnable);
+        return new AnimatorPlaybackController(anim, duration, childAnims);
     }
 
+    // Progress factor after which an animation is considered almost completed.
+    private static final float ANIMATION_COMPLETE_THRESHOLD = 0.95f;
+
     private final ValueAnimator mAnimationPlayer;
     private final long mDuration;
 
-    protected final AnimatorSet mAnim;
-    private Set<SpringAnimation> mSprings;
+    private final AnimatorSet mAnim;
+    private final Holder[] mChildAnimations;
 
     protected float mCurrentFraction;
     private Runnable mEndAction;
@@ -78,22 +74,14 @@
     protected boolean mTargetCancelled = false;
     protected Runnable mOnCancelRunnable;
 
-    private OnAnimationEndDispatcher mEndListener;
-    private DynamicAnimation.OnAnimationEndListener mSpringEndListener;
-    // We need this variable to ensure the end listener is called immediately, otherwise we run into
-    // issues where the callback interferes with the states of the swipe detector.
-    private boolean mSkipToEnd = false;
-
-    protected AnimatorPlaybackController(AnimatorSet anim, long duration,
-            Runnable onCancelRunnable) {
+    /** package private */
+    AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims) {
         mAnim = anim;
         mDuration = duration;
-        mOnCancelRunnable = onCancelRunnable;
 
         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
         mAnimationPlayer.setInterpolator(LINEAR);
-        mEndListener = new OnAnimationEndDispatcher();
-        mAnimationPlayer.addListener(mEndListener);
+        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
         mAnimationPlayer.addUpdateListener(this);
 
         mAnim.addListener(new AnimatorListenerAdapter() {
@@ -118,14 +106,7 @@
             }
         });
 
-        mSprings = new HashSet<>();
-        mSpringEndListener = (animation, canceled, value, velocity1) -> {
-            if (canceled) {
-                mEndListener.onAnimationCancel(mAnimationPlayer);
-            } else {
-                mEndListener.onAnimationEnd(mAnimationPlayer);
-            }
-        };
+        mChildAnimations = childAnims.toArray(new Holder[childAnims.size()]);
     }
 
     public AnimatorSet getTarget() {
@@ -159,9 +140,79 @@
     }
 
     /**
+     * Starts playing the animation with the provided velocity optionally playing any
+     * physics based animations
+     */
+    public void startWithVelocity(Context context, boolean goingToEnd,
+            float velocity, float scale, long animationDuration) {
+        float scaleInverse = 1 / Math.abs(scale);
+        float scaledVelocity = velocity * scaleInverse;
+
+        float nextFrameProgress = boundToRange(getProgressFraction()
+                + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
+
+        // Update setters for spring
+        int springFlag = goingToEnd
+                ? SpringProperty.FLAG_CAN_SPRING_ON_END
+                : SpringProperty.FLAG_CAN_SPRING_ON_START;
+
+        long springDuration = animationDuration;
+        for (Holder h : mChildAnimations) {
+            if ((h.springProperty.flags & springFlag) != 0) {
+                SpringAnimationBuilder s = new SpringAnimationBuilder(context)
+                        .setStartValue(mCurrentFraction)
+                        .setEndValue(goingToEnd ? 1 : 0)
+                        .setStartVelocity(scaledVelocity)
+                        .setMinimumVisibleChange(scaleInverse)
+                        .setDampingRatio(h.springProperty.mDampingRatio)
+                        .setStiffness(h.springProperty.mStiffness)
+                        .computeParams();
+
+                long expectedDurationL = s.getDuration();
+                springDuration = Math.max(expectedDurationL, springDuration);
+
+                float expectedDuration = expectedDurationL;
+                h.mapper = (progress, globalEndProgress) ->
+                        mAnimationPlayer.getCurrentPlayTime() / expectedDuration;
+                h.anim.setInterpolator(s::getInterpolatedValue);
+            }
+        }
+
+        mAnimationPlayer.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
+
+        if (springDuration <= animationDuration) {
+            mAnimationPlayer.setDuration(animationDuration);
+            mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        } else {
+            // Since spring requires more time to run, we let the other animations play with
+            // current time and interpolation and by clamping the duration.
+            mAnimationPlayer.setDuration(springDuration);
+
+            float cutOff = animationDuration / (float) springDuration;
+            mAnimationPlayer.setInterpolator(
+                    clampToProgress(scrollInterpolatorForVelocity(velocity), 0, cutOff));
+        }
+        mAnimationPlayer.start();
+    }
+
+    /**
+     * Tries to finish the running animation if it is close to completion.
+     */
+    public void forceFinishIfCloseToEnd() {
+        if (mAnimationPlayer.isRunning()
+                && mAnimationPlayer.getAnimatedFraction() > ANIMATION_COMPLETE_THRESHOLD) {
+            mAnimationPlayer.end();
+        }
+    }
+
+    /**
      * Pauses the currently playing animation.
      */
     public void pause() {
+        // Reset property setters
+        for (Holder h : mChildAnimations) {
+            h.reset();
+        }
         mAnimationPlayer.cancel();
     }
 
@@ -175,7 +226,18 @@
     /**
      * Sets the current animation position and updates all the child animators accordingly.
      */
-    public abstract void setPlayFraction(float fraction);
+    public void setPlayFraction(float fraction) {
+        mCurrentFraction = fraction;
+        // Let the animator report the progress but don't apply the progress to child
+        // animations if it has been cancelled.
+        if (mTargetCancelled) {
+            return;
+        }
+        float progress = boundToRange(fraction, 0, 1);
+        for (Holder holder : mChildAnimations) {
+            holder.setProgress(progress);
+        }
+    }
 
     public float getProgressFraction() {
         return mCurrentFraction;
@@ -207,169 +269,68 @@
         }
     }
 
-    /**
-     * Starts playback and sets the spring.
-     */
-    public void dispatchOnStartWithVelocity(float end, float velocity) {
-        if (!QUICKSTEP_SPRINGS.get()) {
-            dispatchOnStart();
-            return;
-        }
-
-        if (DEBUG) Log.d(TAG, "dispatchOnStartWithVelocity#end=" + end + ", velocity=" + velocity);
-
-        for (Animator a : mAnim.getChildAnimations()) {
-            if (a instanceof SpringObjectAnimator) {
-                if (DEBUG) Log.d(TAG, "Found springAnimator=" + a);
-                SpringObjectAnimator springAnimator = (SpringObjectAnimator) a;
-                mSprings.add(springAnimator.getSpring());
-                springAnimator.startSpring(end, velocity, mSpringEndListener);
-            }
-        }
-
-        dispatchOnStart();
-    }
-
-    public void dispatchOnStart() {
-        dispatchOnStartRecursively(mAnim);
-    }
-
-    private void dispatchOnStartRecursively(Animator animator) {
-        List<AnimatorListener> listeners = animator instanceof SpringObjectAnimator
-                ? nonNullList(((SpringObjectAnimator) animator).getObjectAnimatorListeners())
-                : nonNullList(animator.getListeners());
-
-        for (AnimatorListener l : listeners) {
-            l.onAnimationStart(animator);
-        }
-
-        if (animator instanceof AnimatorSet) {
-            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                dispatchOnStartRecursively(anim);
-            }
-        }
+    /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */
+    public void dispatchOnCancelWithoutCancelRunnable() {
+        dispatchOnCancelWithoutCancelRunnable(null);
     }
 
     /**
      * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This
      * is intended to be used only if you need to cancel but want to defer cleaning up yourself.
+     * @param callback An optional callback to run after dispatching the cancel but before resetting
+     *                 the onCancelRunnable.
      */
-    public void dispatchOnCancelWithoutCancelRunnable() {
+    public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) {
         Runnable onCancel = mOnCancelRunnable;
         setOnCancelRunnable(null);
         dispatchOnCancel();
+        if (callback != null) {
+            callback.run();
+        }
         setOnCancelRunnable(onCancel);
     }
 
-    public void dispatchOnCancel() {
-        dispatchOnCancelRecursively(mAnim);
+
+    public AnimatorPlaybackController setOnCancelRunnable(Runnable runnable) {
+        mOnCancelRunnable = runnable;
+        return this;
     }
 
-    private void dispatchOnCancelRecursively(Animator animator) {
-        for (AnimatorListener l : nonNullList(animator.getListeners())) {
-            l.onAnimationCancel(animator);
-        }
+    public void dispatchOnStart() {
+        callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
+    }
 
-        if (animator instanceof AnimatorSet) {
-            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                dispatchOnCancelRecursively(anim);
-            }
-        }
+    public void dispatchOnCancel() {
+        callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
     }
 
     public void dispatchSetInterpolator(TimeInterpolator interpolator) {
-        dispatchSetInterpolatorRecursively(mAnim, interpolator);
+        callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator));
     }
 
-    private void dispatchSetInterpolatorRecursively(Animator anim, TimeInterpolator interpolator) {
-        anim.setInterpolator(interpolator);
+    private static void callListenerCommandRecursively(
+            Animator anim, BiConsumer<AnimatorListener, Animator> command) {
+        callAnimatorCommandRecursively(anim, a-> {
+            for (AnimatorListener l : nonNullList(a.getListeners())) {
+                command.accept(l, a);
+            }
+        });
+    }
+
+    private static void callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command) {
+        command.accept(anim);
         if (anim instanceof AnimatorSet) {
             for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) {
-                dispatchSetInterpolatorRecursively(child, interpolator);
+                callAnimatorCommandRecursively(child, command);
             }
         }
     }
 
-    public void setOnCancelRunnable(Runnable runnable) {
-        mOnCancelRunnable = runnable;
-    }
-
-    public void skipToEnd() {
-        mSkipToEnd = true;
-        for (SpringAnimation spring : mSprings) {
-            if (spring.canSkipToEnd()) {
-                spring.skipToEnd();
-            }
-        }
-        mAnimationPlayer.end();
-        mSkipToEnd = false;
-    }
-
-    public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController {
-
-        private final ValueAnimator[] mChildAnimations;
-
-        private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration,
-                Runnable onCancelRunnable) {
-            super(anim, duration, onCancelRunnable);
-
-            // Build animation list
-            ArrayList<ValueAnimator> childAnims = new ArrayList<>();
-            getAnimationsRecur(mAnim, childAnims);
-            mChildAnimations = childAnims.toArray(new ValueAnimator[childAnims.size()]);
-        }
-
-        private void getAnimationsRecur(AnimatorSet anim, ArrayList<ValueAnimator> out) {
-            long forceDuration = anim.getDuration();
-            TimeInterpolator forceInterpolator = anim.getInterpolator();
-            for (Animator child : anim.getChildAnimations()) {
-                if (forceDuration > 0) {
-                    child.setDuration(forceDuration);
-                }
-                if (forceInterpolator != null) {
-                    child.setInterpolator(forceInterpolator);
-                }
-                if (child instanceof ValueAnimator) {
-                    out.add((ValueAnimator) child);
-                } else if (child instanceof AnimatorSet) {
-                    getAnimationsRecur((AnimatorSet) child, out);
-                } else {
-                    throw new RuntimeException("Unknown animation type " + child);
-                }
-            }
-        }
-
-        @Override
-        public void setPlayFraction(float fraction) {
-            mCurrentFraction = fraction;
-            // Let the animator report the progress but don't apply the progress to child
-            // animations if it has been cancelled.
-            if (mTargetCancelled) {
-                return;
-            }
-            long playPos = clampDuration(fraction);
-            for (ValueAnimator anim : mChildAnimations) {
-                anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration()));
-            }
-        }
-    }
-
-    private boolean isAnySpringRunning() {
-        for (SpringAnimation spring : mSprings) {
-            if (spring.isRunning()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /**
      * Only dispatches the on end actions once the animator and all springs have completed running.
      */
     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
-        boolean mAnimatorDone = false;
-        boolean mSpringsDone = false;
         boolean mDispatched = false;
 
         @Override
@@ -380,39 +341,83 @@
 
         @Override
         public void onAnimationSuccess(Animator animator) {
-            if (mSprings.isEmpty()) {
-                mSpringsDone = mAnimatorDone = true;
-            }
-            if (isAnySpringRunning()) {
-                mAnimatorDone = true;
-            } else {
-                mSpringsDone = true;
-            }
-
             // We wait for the spring (if any) to finish running before completing the end callback.
-            if (!mDispatched && (mSkipToEnd || (mAnimatorDone && mSpringsDone))) {
-                dispatchOnEndRecursively(mAnim);
+            if (!mDispatched) {
+                callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
                 if (mEndAction != null) {
                     mEndAction.run();
                 }
                 mDispatched = true;
             }
         }
-
-        private void dispatchOnEndRecursively(Animator animator) {
-            for (AnimatorListener l : nonNullList(animator.getListeners())) {
-                l.onAnimationEnd(animator);
-            }
-
-            if (animator instanceof AnimatorSet) {
-                for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                    dispatchOnEndRecursively(anim);
-                }
-            }
-        }
     }
 
     private static <T> List<T> nonNullList(ArrayList<T> list) {
         return list == null ? Collections.emptyList() : list;
     }
+
+    /**
+     * Interface for mapping progress to animation progress
+     */
+    private interface ProgressMapper {
+
+        ProgressMapper DEFAULT = (progress, globalEndProgress) ->
+                progress > globalEndProgress ? 1 : (progress / globalEndProgress);
+
+        float getProgress(float progress, float globalProgress);
+    }
+
+    /**
+     * Holder class for various child animations
+     */
+    static class Holder {
+
+        public final ValueAnimator anim;
+
+        public final SpringProperty springProperty;
+
+        public final TimeInterpolator interpolator;
+
+        public final float globalEndProgress;
+
+        public ProgressMapper mapper;
+
+        Holder(Animator anim, float globalDuration, SpringProperty springProperty) {
+            this.anim = (ValueAnimator) anim;
+            this.springProperty = springProperty;
+            this.interpolator = this.anim.getInterpolator();
+            this.globalEndProgress = anim.getDuration() / globalDuration;
+            this.mapper = ProgressMapper.DEFAULT;
+        }
+
+        public void setProgress(float progress) {
+            anim.setCurrentFraction(mapper.getProgress(progress, globalEndProgress));
+        }
+
+        public void reset() {
+            anim.setInterpolator(interpolator);
+            mapper = ProgressMapper.DEFAULT;
+        }
+    }
+
+    static void addAnimationHoldersRecur(Animator anim, long globalDuration,
+            SpringProperty springProperty, ArrayList<Holder> out) {
+        long forceDuration = anim.getDuration();
+        TimeInterpolator forceInterpolator = anim.getInterpolator();
+        if (anim instanceof ValueAnimator) {
+            out.add(new Holder(anim, globalDuration, springProperty));
+        } else if (anim instanceof AnimatorSet) {
+            for (Animator child : ((AnimatorSet) anim).getChildAnimations()) {
+                if (forceDuration > 0) {
+                    child.setDuration(forceDuration);
+                }
+                if (forceInterpolator != null) {
+                    child.setInterpolator(forceInterpolator);
+                }
+                addAnimationHoldersRecur(child, globalDuration, springProperty, out);
+            }
+        } else {
+            throw new RuntimeException("Unknown animation type " + anim);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/anim/AnimatorSetBuilder.java b/src/com/android/launcher3/anim/AnimatorSetBuilder.java
deleted file mode 100644
index cd30dea..0000000
--- a/src/com/android/launcher3/anim/AnimatorSetBuilder.java
+++ /dev/null
@@ -1,93 +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.anim;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.util.SparseArray;
-import android.view.animation.Interpolator;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Utility class for building animator set
- */
-public class AnimatorSetBuilder {
-
-    public static final int ANIM_VERTICAL_PROGRESS = 0;
-    public static final int ANIM_WORKSPACE_SCALE = 1;
-    public static final int ANIM_WORKSPACE_TRANSLATE = 2;
-    public static final int ANIM_WORKSPACE_FADE = 3;
-    public static final int ANIM_HOTSEAT_SCALE = 4;
-    public static final int ANIM_HOTSEAT_TRANSLATE = 5;
-    public static final int ANIM_OVERVIEW_SCALE = 6;
-    public static final int ANIM_OVERVIEW_TRANSLATE_X = 7;
-    public static final int ANIM_OVERVIEW_TRANSLATE_Y = 8;
-    public static final int ANIM_OVERVIEW_FADE = 9;
-    public static final int ANIM_ALL_APPS_FADE = 10;
-    public static final int ANIM_OVERVIEW_SCRIM_FADE = 11;
-    public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
-
-    public static final int FLAG_DONT_ANIMATE_OVERVIEW = 1 << 0;
-
-    protected final ArrayList<Animator> mAnims = new ArrayList<>();
-
-    private final SparseArray<Interpolator> mInterpolators = new SparseArray<>();
-    private List<Runnable> mOnFinishRunnables = new ArrayList<>();
-    private int mFlags = 0;
-
-    public void play(Animator anim) {
-        mAnims.add(anim);
-    }
-
-    public void addOnFinishRunnable(Runnable onFinishRunnable) {
-        mOnFinishRunnables.add(onFinishRunnable);
-    }
-
-    public AnimatorSet build() {
-        AnimatorSet anim = new AnimatorSet();
-        anim.playTogether(mAnims);
-        if (!mOnFinishRunnables.isEmpty()) {
-            anim.addListener(new AnimationSuccessListener() {
-                @Override
-                public void onAnimationSuccess(Animator animation) {
-                    for (Runnable onFinishRunnable : mOnFinishRunnables) {
-                        onFinishRunnable.run();
-                    }
-                    mOnFinishRunnables.clear();
-                }
-            });
-        }
-        return anim;
-    }
-
-    public Interpolator getInterpolator(int animId, Interpolator fallback) {
-        return mInterpolators.get(animId, fallback);
-    }
-
-    public void setInterpolator(int animId, Interpolator interpolator) {
-        mInterpolators.put(animId, interpolator);
-    }
-
-    public void addFlag(int flag) {
-        mFlags |= flag;
-    }
-
-    public boolean hasFlag(int flag) {
-        return (mFlags & flag) != 0;
-    }
-}
diff --git a/src/com/android/launcher3/anim/FlingSpringAnim.java b/src/com/android/launcher3/anim/FlingSpringAnim.java
index eaf3b1c..6ea38ec 100644
--- a/src/com/android/launcher3/anim/FlingSpringAnim.java
+++ b/src/com/android/launcher3/anim/FlingSpringAnim.java
@@ -15,32 +15,41 @@
  */
 package com.android.launcher3.anim;
 
+import android.content.Context;
+
 import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
 import androidx.dynamicanimation.animation.FlingAnimation;
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.launcher3.R;
+import com.android.launcher3.util.DynamicResource;
+import com.android.systemui.plugins.ResourceProvider;
+
 /**
  * Given a property to animate and a target value and starting velocity, first apply friction to
  * the fling until we pass the target, then apply a spring force to pull towards the target.
  */
 public class FlingSpringAnim {
 
-    private static final float FLING_FRICTION = 1.5f;
-    private static final float SPRING_STIFFNESS = 200;
-    private static final float SPRING_DAMPING = 0.8f;
-
     private final FlingAnimation mFlingAnim;
     private SpringAnimation mSpringAnim;
+    private final boolean mSkipFlingAnim;
 
     private float mTargetPosition;
 
-    public <K> FlingSpringAnim(K object, FloatPropertyCompat<K> property, float startPosition,
-            float targetPosition, float startVelocity, float minVisChange, float minValue,
-            float maxValue, float springVelocityFactor, OnAnimationEndListener onEndListener) {
+    public <K> FlingSpringAnim(K object, Context context, FloatPropertyCompat<K> property,
+            float startPosition, float targetPosition, float startVelocity, float minVisChange,
+            float minValue, float maxValue, float springVelocityFactor,
+            OnAnimationEndListener onEndListener) {
+        ResourceProvider rp = DynamicResource.provider(context);
+        float damping = rp.getFloat(R.dimen.swipe_up_rect_xy_damping_ratio);
+        float stiffness = rp.getFloat(R.dimen.swipe_up_rect_xy_stiffness);
+        float friction = rp.getFloat(R.dimen.swipe_up_rect_xy_fling_friction);
+
         mFlingAnim = new FlingAnimation(object, property)
-                .setFriction(FLING_FRICTION)
+                .setFriction(friction)
                 // Have the spring pull towards the target if we've slowed down too much before
                 // reaching it.
                 .setMinimumVisibleChange(minVisChange)
@@ -49,13 +58,17 @@
                 .setMaxValue(maxValue);
         mTargetPosition = targetPosition;
 
+        // We are already past the fling target, so skip it to avoid losing a frame of the spring.
+        mSkipFlingAnim = startPosition <= minValue && startVelocity < 0
+                || startPosition >= maxValue && startVelocity > 0;
+
         mFlingAnim.addEndListener(((animation, canceled, value, velocity) -> {
             mSpringAnim = new SpringAnimation(object, property)
                     .setStartValue(value)
                     .setStartVelocity(velocity * springVelocityFactor)
                     .setSpring(new SpringForce(mTargetPosition)
-                            .setStiffness(SPRING_STIFFNESS)
-                            .setDampingRatio(SPRING_DAMPING));
+                            .setStiffness(stiffness)
+                            .setDampingRatio(damping));
             mSpringAnim.addEndListener(onEndListener);
             mSpringAnim.animateToFinalPosition(mTargetPosition);
         }));
@@ -76,6 +89,9 @@
 
     public void start() {
         mFlingAnim.start();
+        if (mSkipFlingAnim) {
+            mFlingAnim.cancel();
+        }
     }
 
     public void end() {
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index fccc120..860cceb 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -61,6 +61,11 @@
     public static final Interpolator EXAGGERATED_EASE;
 
     public static final Interpolator INSTANT = t -> 1;
+    /**
+     * All values of t map to 0 until t == 1. This is primarily useful for setting view visibility,
+     * which should only happen at the very end of the animation (when it's already hidden).
+     */
+    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;
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
new file mode 100644
index 0000000..4195933
--- /dev/null
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -0,0 +1,183 @@
+/*
+ * 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.anim;
+
+import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.util.FloatProperty;
+import android.util.IntProperty;
+import android.view.View;
+
+import com.android.launcher3.anim.AnimatorPlaybackController.Holder;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to keep track of a running animation.
+ *
+ * This class allows attaching end callbacks to an animation is intended to be used with
+ * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case
+ * AnimationListeners are not properly dispatched.
+ *
+ * TODO: Find a better name
+ */
+public class PendingAnimation implements PropertySetter {
+
+    private final ArrayList<Consumer<EndState>> mEndListeners = new ArrayList<>();
+
+    private final ArrayList<Holder> mAnimHolders = new ArrayList<>();
+    private final AnimatorSet mAnim;
+    private final long mDuration;
+
+    private ValueAnimator mProgressAnimator;
+
+    public PendingAnimation(long  duration) {
+        mDuration = duration;
+        mAnim = new AnimatorSet();
+    }
+
+    /**
+     * Utility method to sent an interpolator on an animation and add it to the list
+     */
+    public void add(Animator anim, TimeInterpolator interpolator, SpringProperty springProperty) {
+        anim.setInterpolator(interpolator);
+        add(anim, springProperty);
+    }
+
+    public void add(Animator anim) {
+        add(anim, SpringProperty.DEFAULT);
+    }
+
+    public void add(Animator a, SpringProperty springProperty) {
+        mAnim.play(a.setDuration(mDuration));
+        addAnimationHoldersRecur(a, mDuration, springProperty, mAnimHolders);
+    }
+
+    public void finish(boolean isSuccess, int logAction) {
+        for (Consumer<EndState> listeners : mEndListeners) {
+            listeners.accept(new EndState(isSuccess, logAction));
+        }
+        mEndListeners.clear();
+    }
+
+    @Override
+    public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
+        if (view == null || view.getAlpha() == alpha) {
+            return;
+        }
+        ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha);
+        anim.addListener(new AlphaUpdateListener(view));
+        anim.setInterpolator(interpolator);
+        add(anim);
+    }
+
+    @Override
+    public <T> void setFloat(T target, FloatProperty<T> property, float value,
+            TimeInterpolator interpolator) {
+        if (property.get(target) == value) {
+            return;
+        }
+        Animator anim = ObjectAnimator.ofFloat(target, property, value);
+        anim.setDuration(mDuration).setInterpolator(interpolator);
+        add(anim);
+    }
+
+    public <T> void addFloat(T target, FloatProperty<T> property, float from, float to,
+            TimeInterpolator interpolator) {
+        Animator anim = ObjectAnimator.ofFloat(target, property, from, to);
+        anim.setInterpolator(interpolator);
+        add(anim);
+    }
+
+    @Override
+    public <T> void setInt(T target, IntProperty<T> property, int value,
+            TimeInterpolator interpolator) {
+        if (property.get(target) == value) {
+            return;
+        }
+        Animator anim = ObjectAnimator.ofInt(target, property, value);
+        anim.setInterpolator(interpolator);
+        add(anim);
+    }
+
+    /**
+     * Adds a callback to be run on every frame of the animation
+     */
+    public void addOnFrameCallback(Runnable runnable) {
+        if (mProgressAnimator == null) {
+            mProgressAnimator = ValueAnimator.ofFloat(0, 1);
+        }
+
+        mProgressAnimator.addUpdateListener(anim -> runnable.run());
+    }
+
+    /**
+     * @see AnimatorSet#addListener(AnimatorListener)
+     */
+    public void addListener(Animator.AnimatorListener listener) {
+        mAnim.addListener(listener);
+    }
+
+    /**
+     * Creates and returns the underlying AnimatorSet
+     */
+    public AnimatorSet buildAnim() {
+        // Add progress animation to the end, so that frame callback is called after all the other
+        // animation update.
+        if (mProgressAnimator != null) {
+            add(mProgressAnimator);
+            mProgressAnimator = null;
+        }
+        if (mAnimHolders.isEmpty()) {
+            // Add a dummy animation to that the duration is respected
+            add(ValueAnimator.ofFloat(0, 1).setDuration(mDuration));
+        }
+        return mAnim;
+    }
+
+    /**
+     * Creates a controller for this animation
+     */
+    public AnimatorPlaybackController createPlaybackController() {
+        return new AnimatorPlaybackController(buildAnim(), mDuration, mAnimHolders);
+    }
+
+    /**
+     * Add a listener of receiving the end state.
+     * Note that the listeners are called as a result of calling {@link #finish(boolean, int)}
+     * and not automatically
+     */
+    public void addEndListener(Consumer<EndState> listener) {
+        mEndListeners.add(listener);
+    }
+
+    public static class EndState {
+        public boolean isSuccess;
+        public int logAction;
+
+        public EndState(boolean isSuccess, int logAction) {
+            this.isSuccess = isSuccess;
+            this.logAction = logAction;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
index 757edff..2ce620b 100644
--- a/src/com/android/launcher3/anim/PropertySetter.java
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -16,77 +16,43 @@
 
 package com.android.launcher3.anim;
 
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.util.Property;
+import android.util.FloatProperty;
+import android.util.IntProperty;
 import android.view.View;
 
 /**
  * Utility class for setting a property with or without animation
  */
-public class PropertySetter {
+public interface PropertySetter {
 
-    public static final PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter();
+    PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter() { };
 
-    public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
+    /**
+     * Sets the view alpha using the provided interpolator.
+     * Unlike {@link #setFloat}, this also updates the visibility of the view as alpha changes
+     * between zero and non-zero.
+     */
+    default void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
         if (view != null) {
             view.setAlpha(alpha);
             AlphaUpdateListener.updateVisibility(view);
         }
     }
 
-    public <T> void setFloat(T target, Property<T, Float> property, float value,
+    /**
+     * Updates the float property of the target using the provided interpolator
+     */
+    default <T> void setFloat(T target, FloatProperty<T> property, float value,
             TimeInterpolator interpolator) {
-        property.set(target, value);
+        property.setValue(target, value);
     }
 
-    public <T> void setInt(T target, Property<T, Integer> property, int value,
+    /**
+     * Updates the int property of the target using the provided interpolator
+     */
+    default <T> void setInt(T target, IntProperty<T> property, int value,
             TimeInterpolator interpolator) {
-        property.set(target, value);
-    }
-
-    public static class AnimatedPropertySetter extends PropertySetter {
-
-        private final long mDuration;
-        private final AnimatorSetBuilder mStateAnimator;
-
-        public AnimatedPropertySetter(long duration, AnimatorSetBuilder builder) {
-            mDuration = duration;
-            mStateAnimator = builder;
-        }
-
-        @Override
-        public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
-            if (view == null || view.getAlpha() == alpha) {
-                return;
-            }
-            ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha);
-            anim.addListener(new AlphaUpdateListener(view));
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
-
-        @Override
-        public <T> void setFloat(T target, Property<T, Float> property, float value,
-                TimeInterpolator interpolator) {
-            if (property.get(target) == value) {
-                return;
-            }
-            Animator anim = ObjectAnimator.ofFloat(target, property, value);
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
-
-        @Override
-        public <T> void setInt(T target, Property<T, Integer> property, int value,
-                TimeInterpolator interpolator) {
-            if (property.get(target) == value) {
-                return;
-            }
-            Animator anim = ObjectAnimator.ofInt(target, property, value);
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
+        property.setValue(target, value);
     }
 }
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index 0f34c1e..a9702b4 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -15,24 +15,25 @@
  */
 package com.android.launcher3.anim;
 
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
 import android.animation.Animator;
-import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.FloatProperty;
 
-import com.android.launcher3.util.DefaultDisplay;
-
 import androidx.annotation.FloatRange;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.launcher3.util.DefaultDisplay;
+
 /**
  * Utility class to build an object animator which follows the same path as a spring animation for
  * an underdamped spring.
  */
-public class SpringAnimationBuilder<T> extends FloatProperty<T> {
+public class SpringAnimationBuilder {
 
-    private final T mTarget;
-    private final FloatProperty<T> mProperty;
+    private final Context mContext;
 
     private float mStartValue;
     private float mEndValue;
@@ -65,27 +66,23 @@
     private double mValueThreshold;
     private double mVelocityThreshold;
 
-    private float mCurrentTime = 0;
+    private float mDuration = 0;
 
-    public SpringAnimationBuilder(T target, FloatProperty<T> property) {
-        super("dynamic-spring-property");
-        mTarget = target;
-        mProperty = property;
-
-        mStartValue = mProperty.get(target);
+    public SpringAnimationBuilder(Context context) {
+        mContext = context;
     }
 
-    public SpringAnimationBuilder<T> setEndValue(float value) {
+    public SpringAnimationBuilder setEndValue(float value) {
         mEndValue = value;
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStartValue(float value) {
+    public SpringAnimationBuilder setStartValue(float value) {
         mStartValue = value;
         return this;
     }
 
-    public SpringAnimationBuilder<T> setValues(float... values) {
+    public SpringAnimationBuilder setValues(float... values) {
         if (values.length > 1) {
             mStartValue = values[0];
             mEndValue = values[values.length - 1];
@@ -95,7 +92,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStiffness(
+    public SpringAnimationBuilder setStiffness(
             @FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
         if (stiffness <= 0) {
             throw new IllegalArgumentException("Spring stiffness constant must be positive.");
@@ -104,7 +101,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setDampingRatio(
+    public SpringAnimationBuilder setDampingRatio(
             @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
                     float dampingRatio) {
         if (dampingRatio <= 0 || dampingRatio >= 1) {
@@ -114,7 +111,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setMinimumVisibleChange(
+    public SpringAnimationBuilder setMinimumVisibleChange(
             @FloatRange(from = 0.0, fromInclusive = false) float minimumVisibleChange) {
         if (minimumVisibleChange <= 0) {
             throw new IllegalArgumentException("Minimum visible change must be positive.");
@@ -123,25 +120,21 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStartVelocity(float startVelocity) {
+    public SpringAnimationBuilder setStartVelocity(float startVelocity) {
         mVelocity = startVelocity;
         return this;
     }
 
-    @Override
-    public void setValue(T object, float time) {
-        mCurrentTime = time;
-        mProperty.setValue(
-                object, (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue);
+    public float getInterpolatedValue(float fraction) {
+        return getValue(mDuration * fraction);
     }
 
-    @Override
-    public Float get(T t) {
-        return mCurrentTime;
+    private float getValue(float time) {
+        return (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue;
     }
 
-    public ObjectAnimator build(Context context) {
-        int singleFrameMs = DefaultDisplay.getSingleFrameMs(context);
+    public SpringAnimationBuilder computeParams() {
+        int singleFrameMs = DefaultDisplay.getSingleFrameMs(mContext);
         double naturalFreq = Math.sqrt(mStiffness);
         double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
 
@@ -188,14 +181,25 @@
             }
         } while (true);
 
+        mDuration = (float) duration;
+        return this;
+    }
 
-        long durationMs = (long) (1000.0 * duration);
-        ObjectAnimator animator = ObjectAnimator.ofFloat(mTarget, this, 0, (float) duration);
-        animator.setDuration(durationMs).setInterpolator(Interpolators.LINEAR);
+    public long getDuration() {
+        return (long) (1000.0 * mDuration);
+    }
+
+    public <T> ValueAnimator build(T target, FloatProperty<T> property) {
+        computeParams();
+
+        ValueAnimator animator = ValueAnimator.ofFloat(0, mDuration);
+        animator.setDuration(getDuration()).setInterpolator(LINEAR);
+        animator.addUpdateListener(anim ->
+                property.set(target, getInterpolatedValue(anim.getAnimatedFraction())));
         animator.addListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationSuccess(Animator animator) {
-                mProperty.setValue(mTarget, mEndValue);
+            public void onAnimationSuccess(Animator animation) {
+                property.set(target, mEndValue);
             }
         });
         return animator;
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
deleted file mode 100644
index 91a3106..0000000
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.anim;
-
-import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat;
-
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.FloatProperty;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
-/**
- * This animator allows for an object's property to be be controlled by an {@link ObjectAnimator} or
- * a {@link SpringAnimation}. It extends ValueAnimator so it can be used in an AnimatorSet.
- */
-public class SpringObjectAnimator<T> extends ValueAnimator {
-
-    private static final String TAG = "SpringObjectAnimator";
-    private static boolean DEBUG = false;
-
-    private ObjectAnimator mObjectAnimator;
-    private float[] mValues;
-
-    private SpringAnimation mSpring;
-    private SpringProperty<T> mProperty;
-
-    private ArrayList<AnimatorListener> mListeners;
-    private boolean mSpringEnded = true;
-    private boolean mAnimatorEnded = true;
-    private boolean mEnded = true;
-
-    public SpringObjectAnimator(T object, FloatProperty<T> property, float minimumVisibleChange,
-            float damping, float stiffness, float... values) {
-        mSpring = new SpringAnimation(object, createFloatPropertyCompat(property));
-        mSpring.setMinimumVisibleChange(minimumVisibleChange);
-        mSpring.setSpring(new SpringForce(0)
-                .setDampingRatio(damping)
-                .setStiffness(stiffness));
-        mSpring.setStartVelocity(0.01f);
-        mProperty = new SpringProperty<>(property, mSpring);
-        mObjectAnimator = ObjectAnimator.ofFloat(object, mProperty, values);
-        mValues = values;
-        mListeners = new ArrayList<>();
-        setFloatValues(values);
-
-        // We use this listener and track mListeners so that we can sync the animator and spring
-        // listeners.
-        mObjectAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mAnimatorEnded = false;
-                mEnded = false;
-                for (AnimatorListener l : mListeners) {
-                    l.onAnimationStart(animation);
-                }
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimatorEnded = true;
-                tryEnding();
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                for (AnimatorListener l : mListeners) {
-                    l.onAnimationCancel(animation);
-                }
-                mSpring.cancel();
-            }
-        });
-
-        mSpring.addUpdateListener((animation, value, velocity) -> {
-            mSpringEnded = false;
-            mEnded = false;
-        });
-        mSpring.addEndListener((animation, canceled, value, velocity) -> {
-            mSpringEnded = true;
-            tryEnding();
-        });
-    }
-
-    private void tryEnding() {
-        if (DEBUG) {
-            Log.d(TAG, "tryEnding#mAnimatorEnded=" + mAnimatorEnded + ", mSpringEnded="
-                    + mSpringEnded + ", mEnded=" + mEnded);
-        }
-
-        // If springs are disabled, ignore value of mSpringEnded
-        if (mAnimatorEnded && (mSpringEnded || !QUICKSTEP_SPRINGS.get()) && !mEnded) {
-            for (AnimatorListener l : mListeners) {
-                l.onAnimationEnd(this);
-            }
-            mEnded = true;
-        }
-    }
-
-    public SpringAnimation getSpring() {
-        return mSpring;
-    }
-
-    /**
-     * Initializes and sets up the spring to take over controlling the object.
-     */
-    public void startSpring(float end, float velocity, OnAnimationEndListener endListener) {
-        // Cancel the spring so we can set new start velocity and final position. We need to remove
-        // the listener since the spring is not actually ending.
-        mSpring.removeEndListener(endListener);
-        mSpring.cancel();
-        mSpring.addEndListener(endListener);
-
-        mProperty.switchToSpring();
-
-        mSpring.setStartVelocity(velocity);
-
-        float startValue = end == 0 ? mValues[1] : mValues[0];
-        float endValue = end == 0 ? mValues[0] : mValues[1];
-        mSpring.setStartValue(startValue);
-        new Handler(Looper.getMainLooper()).postDelayed(() -> {
-            mSpring.animateToFinalPosition(endValue);
-        }, getStartDelay());
-    }
-
-    @Override
-    public void addListener(AnimatorListener listener) {
-        mListeners.add(listener);
-    }
-
-    public ArrayList<AnimatorListener> getObjectAnimatorListeners() {
-        return mObjectAnimator.getListeners();
-    }
-
-    @Override
-    public ArrayList<AnimatorListener> getListeners() {
-        return mListeners;
-    }
-
-    @Override
-    public void removeAllListeners() {
-        mListeners.clear();
-    }
-
-    @Override
-    public void removeListener(AnimatorListener listener) {
-        mListeners.remove(listener);
-    }
-
-    @Override
-    public void addPauseListener(AnimatorPauseListener listener) {
-        mObjectAnimator.addPauseListener(listener);
-    }
-
-    @Override
-    public void cancel() {
-        mObjectAnimator.cancel();
-        mSpring.cancel();
-    }
-
-    @Override
-    public void end() {
-        mObjectAnimator.end();
-    }
-
-    @Override
-    public long getDuration() {
-        return mObjectAnimator.getDuration();
-    }
-
-    @Override
-    public TimeInterpolator getInterpolator() {
-        return mObjectAnimator.getInterpolator();
-    }
-
-    @Override
-    public long getStartDelay() {
-        return mObjectAnimator.getStartDelay();
-    }
-
-    @Override
-    public long getTotalDuration() {
-        return mObjectAnimator.getTotalDuration();
-    }
-
-    @Override
-    public boolean isPaused() {
-        return mObjectAnimator.isPaused();
-    }
-
-    @Override
-    public boolean isRunning() {
-        return mObjectAnimator.isRunning();
-    }
-
-    @Override
-    public boolean isStarted() {
-        return mObjectAnimator.isStarted();
-    }
-
-    @Override
-    public void pause() {
-        mObjectAnimator.pause();
-    }
-
-    @Override
-    public void removePauseListener(AnimatorPauseListener listener) {
-        mObjectAnimator.removePauseListener(listener);
-    }
-
-    @Override
-    public void resume() {
-        mObjectAnimator.resume();
-    }
-
-    @Override
-    public ValueAnimator setDuration(long duration) {
-        return mObjectAnimator.setDuration(duration);
-    }
-
-    @Override
-    public void setInterpolator(TimeInterpolator value) {
-        mObjectAnimator.setInterpolator(value);
-    }
-
-    @Override
-    public void setStartDelay(long startDelay) {
-        mObjectAnimator.setStartDelay(startDelay);
-    }
-
-    @Override
-    public void setTarget(Object target) {
-        mObjectAnimator.setTarget(target);
-    }
-
-    @Override
-    public void start() {
-        mObjectAnimator.start();
-    }
-
-    @Override
-    public void setCurrentFraction(float fraction) {
-        mObjectAnimator.setCurrentFraction(fraction);
-    }
-
-    @Override
-    public void setCurrentPlayTime(long playTime) {
-        mObjectAnimator.setCurrentPlayTime(playTime);
-    }
-
-    public static class SpringProperty<T> extends FloatProperty<T> {
-
-        boolean useSpring = false;
-        final FloatProperty<T> mProperty;
-        final SpringAnimation mSpring;
-
-        public SpringProperty(FloatProperty<T> property, SpringAnimation spring) {
-            super(property.getName());
-            mProperty = property;
-            mSpring = spring;
-        }
-
-        public void switchToSpring() {
-            useSpring = true;
-        }
-
-        @Override
-        public Float get(T object) {
-            return mProperty.get(object);
-        }
-
-        @Override
-        public void setValue(T object, float progress) {
-            if (useSpring) {
-                mSpring.animateToFinalPosition(progress);
-            } else {
-                mProperty.setValue(object, progress);
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/anim/SpringProperty.java b/src/com/android/launcher3/anim/SpringProperty.java
new file mode 100644
index 0000000..caedd6c
--- /dev/null
+++ b/src/com/android/launcher3/anim/SpringProperty.java
@@ -0,0 +1,54 @@
+/*
+ * 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.anim;
+
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * Utility class to store configurations for spring animation
+ */
+public class SpringProperty {
+
+    public static final SpringProperty DEFAULT = new SpringProperty();
+
+    // Play spring when the animation is going towards the end
+    public static final int FLAG_CAN_SPRING_ON_END = 1 << 0;
+    // Play spring when animation is going towards the start (in reverse direction)
+    public static final int FLAG_CAN_SPRING_ON_START = 1 << 1;
+
+    public final int flags;
+
+    float mDampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
+    float mStiffness = SpringForce.STIFFNESS_MEDIUM;
+
+    public SpringProperty() {
+        this(0);
+    }
+
+    public SpringProperty(int flags) {
+        this.flags = flags;
+    }
+
+    public SpringProperty setDampingRatio(float dampingRatio) {
+        mDampingRatio = dampingRatio;
+        return this;
+    }
+
+    public SpringProperty setStiffness(float stiffness) {
+        mStiffness = stiffness;
+        return this;
+    }
+}
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 81c95cb..1d32d1d 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -18,15 +18,16 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityNodeInfo;
 
-import com.android.launcher3.testing.TestProtocol;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Utilities;
-
-import java.util.function.Consumer;
+import com.android.launcher3.testing.TestProtocol;
 
 public class AccessibilityManagerCompat {
 
@@ -39,11 +40,21 @@
         return isAccessibilityEnabled(context);
     }
 
-    public static void sendCustomAccessibilityEvent(View target, int type, String text) {
-        if (isObservedEventType(target.getContext(), type)) {
+    /**
+     *
+     * @param target The view the accessibility event is initialized on.
+     *               If null, this method has no effect.
+     * @param type See TYPE_ constants defined in {@link AccessibilityEvent}.
+     * @param text Optional text to add to the event, which will be announced to the user.
+     */
+    public static void sendCustomAccessibilityEvent(@Nullable View target, int type,
+            @Nullable String text) {
+        if (target != null && isObservedEventType(target.getContext(), type)) {
             AccessibilityEvent event = AccessibilityEvent.obtain(type);
             target.onInitializeAccessibilityEvent(event);
-            event.getText().add(text);
+            if (!TextUtils.isEmpty(text)) {
+                event.getText().add(text);
+            }
             getManager(target.getContext()).sendAccessibilityEvent(event);
         }
     }
@@ -60,6 +71,7 @@
         parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
 
         sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+        Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
@@ -103,24 +115,6 @@
         return accessibilityManager;
     }
 
-    public static boolean processTestRequest(Context context, String eventTag, int action,
-            Bundle request, Consumer<Bundle> responseFiller) {
-        final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
-        if (accessibilityManager == null) return false;
-
-        // The test sends a request via a ACTION_SET_TEXT.
-        if (action == AccessibilityNodeInfo.ACTION_SET_TEXT &&
-                eventTag.equals(request.getCharSequence(
-                        AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE))) {
-            final Bundle response = new Bundle();
-            responseFiller.accept(response);
-            AccessibilityManagerCompat.sendEventToTest(
-                    accessibilityManager, eventTag + TestProtocol.RESPONSE_MESSAGE_POSTFIX, response);
-            return true;
-        }
-        return false;
-    }
-
     public static int getRecommendedTimeoutMillis(Context context, int originalTimeout, int flags) {
         if (Utilities.ATLEAST_Q) {
             return getManager(context).getRecommendedTimeoutMillis(originalTimeout, flags);
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
deleted file mode 100644
index fc5d11c..0000000
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
-
-import java.util.HashMap;
-import java.util.List;
-
-public abstract class AppWidgetManagerCompat {
-
-    private static final Object sInstanceLock = new Object();
-    private static AppWidgetManagerCompat sInstance;
-
-    public static AppWidgetManagerCompat getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                if (Utilities.ATLEAST_OREO) {
-                    sInstance = new AppWidgetManagerCompatVO(context.getApplicationContext());
-                } else {
-                    sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
-                }
-            }
-            return sInstance;
-        }
-    }
-
-    final AppWidgetManager mAppWidgetManager;
-    final Context mContext;
-
-    AppWidgetManagerCompat(Context context) {
-        mContext = context;
-        mAppWidgetManager = AppWidgetManager.getInstance(context);
-    }
-
-    public LauncherAppWidgetProviderInfo getLauncherAppWidgetInfo(int appWidgetId) {
-        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
-            return CustomWidgetManager.INSTANCE.get(mContext).getWidgetProvider(appWidgetId);
-        }
-        AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
-        return info == null ? null : LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
-    }
-
-    public abstract List<AppWidgetProviderInfo> getAllProviders(
-            @Nullable PackageUserKey packageUser);
-
-    public abstract boolean bindAppWidgetIdIfAllowed(
-            int appWidgetId, AppWidgetProviderInfo info, Bundle options);
-
-    public abstract LauncherAppWidgetProviderInfo findProvider(
-            ComponentName provider, UserHandle user);
-
-    public abstract HashMap<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap();
-}
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
deleted file mode 100644
index c8b1f67..0000000
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.custom.CustomAppWidgetProviderInfo;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
-class AppWidgetManagerCompatVL extends AppWidgetManagerCompat {
-
-    private final UserManager mUserManager;
-
-    AppWidgetManagerCompatVL(Context context) {
-        super(context);
-        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-    }
-
-    @Override
-    public List<AppWidgetProviderInfo> getAllProviders(@Nullable PackageUserKey packageUser) {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
-            return Collections.emptyList();
-        }
-        if (packageUser == null) {
-            ArrayList<AppWidgetProviderInfo> providers = new ArrayList<>();
-            for (UserHandle user : mUserManager.getUserProfiles()) {
-                providers.addAll(mAppWidgetManager.getInstalledProvidersForProfile(user));
-            }
-            providers.addAll(getCustomWidgets());
-            return providers;
-        }
-        // Only get providers for the given package/user.
-        List<AppWidgetProviderInfo> providers = new ArrayList<>(mAppWidgetManager
-                .getInstalledProvidersForProfile(packageUser.mUser));
-        Iterator<AppWidgetProviderInfo> iterator = providers.iterator();
-        while (iterator.hasNext()) {
-            if (!iterator.next().provider.getPackageName().equals(packageUser.mPackageName)) {
-                iterator.remove();
-            }
-        }
-
-        if (Process.myUserHandle().equals(packageUser.mUser)
-                && mContext.getPackageName().equals(packageUser.mPackageName)) {
-            providers.addAll(getCustomWidgets());
-        }
-        return providers;
-    }
-
-    @Override
-    public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info,
-            Bundle options) {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
-            return false;
-        }
-        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
-            return true;
-        }
-        return mAppWidgetManager.bindAppWidgetIdIfAllowed(
-                appWidgetId, info.getProfile(), info.provider, options);
-    }
-
-    @Override
-    public LauncherAppWidgetProviderInfo findProvider(ComponentName provider, UserHandle user) {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
-            return null;
-        }
-        for (AppWidgetProviderInfo info :
-                getAllProviders(new PackageUserKey(provider.getPackageName(), user))) {
-            if (info.provider.equals(provider)) {
-                return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
-            }
-        }
-
-        if (Process.myUserHandle().equals(user)) {
-            for (LauncherAppWidgetProviderInfo info : getCustomWidgets()) {
-                if (info.provider.equals(provider)) {
-                    return info;
-                }
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public HashMap<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap() {
-        HashMap<ComponentKey, AppWidgetProviderInfo> result = new HashMap<>();
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
-            return result;
-        }
-        for (UserHandle user : mUserManager.getUserProfiles()) {
-            for (AppWidgetProviderInfo info :
-                    mAppWidgetManager.getInstalledProvidersForProfile(user)) {
-                result.put(new ComponentKey(info.provider, user), info);
-            }
-        }
-        for (LauncherAppWidgetProviderInfo info : getCustomWidgets()) {
-            result.put(new ComponentKey(info.provider, info.getProfile()), info);
-        }
-        return result;
-    }
-
-    List<CustomAppWidgetProviderInfo> getCustomWidgets() {
-        return CustomWidgetManager.INSTANCE.get(mContext).getCustomWidgets();
-    }
-}
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java
deleted file mode 100644
index 11ec333..0000000
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java
+++ /dev/null
@@ -1,47 +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.compat;
-
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.PackageUserKey;
-
-import java.util.Collections;
-import java.util.List;
-
-class AppWidgetManagerCompatVO extends AppWidgetManagerCompatVL {
-
-    AppWidgetManagerCompatVO(Context context) {
-        super(context);
-    }
-
-    @Override
-    public List<AppWidgetProviderInfo> getAllProviders(@Nullable PackageUserKey packageUser) {
-        if (FeatureFlags.GO_DISABLE_WIDGETS) {
-            return Collections.emptyList();
-        }
-        if (packageUser == null) {
-            return super.getAllProviders(null);
-        }
-        return mAppWidgetManager.getInstalledProvidersForPackage(packageUser.mPackageName,
-                packageUser.mUser);
-    }
-}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
deleted file mode 100644
index 39f6949..0000000
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-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.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionCallback;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.PackageUserKey;
-
-import java.util.List;
-
-public abstract class LauncherAppsCompat {
-
-    public interface OnAppsChangedCallbackCompat {
-        default void onPackageRemoved(String packageName, UserHandle user) { }
-        default void onPackageAdded(String packageName, UserHandle user) { }
-        default void onPackageChanged(String packageName, UserHandle user) { }
-        default void onPackagesAvailable(String[] packageNames, UserHandle user,
-                boolean replacing) { }
-        default void onPackagesUnavailable(String[] packageNames, UserHandle user,
-                boolean replacing) { }
-        default void onPackagesSuspended(String[] packageNames, UserHandle user) { }
-        default void onPackagesUnsuspended(String[] packageNames, UserHandle user) { }
-        default void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
-                UserHandle user) { }
-    }
-
-    protected LauncherAppsCompat() {
-    }
-
-    private static LauncherAppsCompat sInstance;
-    private static final Object sInstanceLock = new Object();
-
-    public static LauncherAppsCompat getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                if (Utilities.ATLEAST_Q) {
-                    sInstance = new LauncherAppsCompatVQ(context.getApplicationContext());
-                } else if (Utilities.ATLEAST_OREO) {
-                    sInstance = new LauncherAppsCompatVO(context.getApplicationContext());
-                } else {
-                    sInstance = new LauncherAppsCompatVL(context.getApplicationContext());
-                }
-            }
-            return sInstance;
-        }
-    }
-
-    public abstract List<LauncherActivityInfo> getActivityList(String packageName,
-            UserHandle user);
-    public abstract LauncherActivityInfo resolveActivity(Intent intent,
-            UserHandle user);
-    public abstract void startActivityForProfile(ComponentName component, UserHandle user,
-            Rect sourceBounds, Bundle opts);
-    public abstract ApplicationInfo getApplicationInfo(
-            String packageName, int flags, UserHandle user);
-    public abstract void showAppDetailsForProfile(ComponentName component, UserHandle user,
-            Rect sourceBounds, Bundle opts);
-    public abstract void addOnAppsChangedCallback(OnAppsChangedCallbackCompat listener);
-    public abstract void removeOnAppsChangedCallback(OnAppsChangedCallbackCompat listener);
-    public abstract boolean isPackageEnabledForProfile(String packageName, UserHandle user);
-    public abstract boolean isActivityEnabledForProfile(ComponentName component,
-            UserHandle user);
-    public abstract List<ShortcutConfigActivityInfo> getCustomShortcutActivityList(
-            @Nullable PackageUserKey packageUser);
-
-    public abstract List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions();
-
-    public abstract void registerSessionCallback(LooperExecutor executor,
-                                                 SessionCallback sessionCallback);
-    public abstract void unregisterSessionCallback(SessionCallback sessionCallback);
-}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
deleted file mode 100644
index 281274c..0000000
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-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.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionCallback;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.launcher3.compat.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVL;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.PackageUserKey;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class LauncherAppsCompatVL extends LauncherAppsCompat {
-
-    protected final LauncherApps mLauncherApps;
-    protected final Context mContext;
-
-    private final ArrayMap<OnAppsChangedCallbackCompat, WrappedCallback> mCallbacks =
-        new ArrayMap<>();
-
-    LauncherAppsCompatVL(Context context) {
-        mContext = context;
-        mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
-    }
-
-    @Override
-    public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
-        return mLauncherApps.getActivityList(packageName, user);
-    }
-
-    @Override
-    public LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
-        return mLauncherApps.resolveActivity(intent, user);
-    }
-
-    @Override
-    public void startActivityForProfile(ComponentName component, UserHandle user,
-            Rect sourceBounds, Bundle opts) {
-        mLauncherApps.startMainActivity(component, user, sourceBounds, opts);
-    }
-
-    @Override
-    public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user) {
-        final boolean isPrimaryUser = Process.myUserHandle().equals(user);
-        if (!isPrimaryUser && (flags == 0)) {
-            // We are looking for an installed app on a secondary profile. Prior to O, the only
-            // entry point for work profiles is through the LauncherActivity.
-            List<LauncherActivityInfo> activityList =
-                    mLauncherApps.getActivityList(packageName, user);
-            return activityList.size() > 0 ? activityList.get(0).getApplicationInfo() : null;
-        }
-        try {
-            ApplicationInfo info =
-                    mContext.getPackageManager().getApplicationInfo(packageName, flags);
-            // There is no way to check if the app is installed for managed profile. But for
-            // primary profile, we can still have this check.
-            if (isPrimaryUser && ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0)
-                    || !info.enabled) {
-                return null;
-            }
-            return info;
-        } catch (PackageManager.NameNotFoundException e) {
-            // Package not found
-            return null;
-        }
-    }
-
-    @Override
-    public void showAppDetailsForProfile(ComponentName component, UserHandle user,
-            Rect sourceBounds, Bundle opts) {
-        mLauncherApps.startAppDetailsActivity(component, user, sourceBounds, opts);
-    }
-
-    @Override
-    public void addOnAppsChangedCallback(LauncherAppsCompat.OnAppsChangedCallbackCompat callback) {
-        WrappedCallback wrappedCallback = new WrappedCallback(callback);
-        synchronized (mCallbacks) {
-            mCallbacks.put(callback, wrappedCallback);
-        }
-        mLauncherApps.registerCallback(wrappedCallback);
-    }
-
-    @Override
-    public void removeOnAppsChangedCallback(OnAppsChangedCallbackCompat callback) {
-        final WrappedCallback wrappedCallback;
-        synchronized (mCallbacks) {
-            wrappedCallback = mCallbacks.remove(callback);
-        }
-        if (wrappedCallback != null) {
-            mLauncherApps.unregisterCallback(wrappedCallback);
-        }
-    }
-
-    @Override
-    public boolean isPackageEnabledForProfile(String packageName, UserHandle user) {
-        return mLauncherApps.isPackageEnabled(packageName, user);
-    }
-
-    @Override
-    public boolean isActivityEnabledForProfile(ComponentName component, UserHandle user) {
-        return mLauncherApps.isActivityEnabled(component, user);
-    }
-
-    private static class WrappedCallback extends LauncherApps.Callback {
-        private final LauncherAppsCompat.OnAppsChangedCallbackCompat mCallback;
-
-        public WrappedCallback(LauncherAppsCompat.OnAppsChangedCallbackCompat callback) {
-            mCallback = callback;
-        }
-
-        @Override
-        public void onPackageRemoved(String packageName, UserHandle user) {
-            mCallback.onPackageRemoved(packageName, user);
-        }
-
-        @Override
-        public void onPackageAdded(String packageName, UserHandle user) {
-            mCallback.onPackageAdded(packageName, user);
-        }
-
-        @Override
-        public void onPackageChanged(String packageName, UserHandle user) {
-            mCallback.onPackageChanged(packageName, user);
-        }
-
-        @Override
-        public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
-            mCallback.onPackagesAvailable(packageNames, user, replacing);
-        }
-
-        @Override
-        public void onPackagesUnavailable(String[] packageNames, UserHandle user,
-                boolean replacing) {
-            mCallback.onPackagesUnavailable(packageNames, user, replacing);
-        }
-
-        @Override
-        public void onPackagesSuspended(String[] packageNames, UserHandle user) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.APP_NOT_DISABLED, "onPackagesSuspended: " +
-                        Arrays.toString(packageNames));
-            }
-            mCallback.onPackagesSuspended(packageNames, user);
-        }
-
-        @Override
-        public void onPackagesUnsuspended(String[] packageNames, UserHandle user) {
-            mCallback.onPackagesUnsuspended(packageNames, user);
-        }
-
-        @Override
-        public void onShortcutsChanged(@NonNull String packageName,
-            @NonNull List<ShortcutInfo> shortcuts,
-            @NonNull UserHandle user) {
-            mCallback.onShortcutsChanged(packageName, shortcuts, user);
-        }
-    }
-
-    @Override
-    public List<ShortcutConfigActivityInfo> getCustomShortcutActivityList(
-            @Nullable PackageUserKey packageUser) {
-        List<ShortcutConfigActivityInfo> result = new ArrayList<>();
-        if (packageUser != null && !packageUser.mUser.equals(Process.myUserHandle())) {
-            return result;
-        }
-        PackageManager pm = mContext.getPackageManager();
-        for (ResolveInfo info :
-                pm.queryIntentActivities(new Intent(Intent.ACTION_CREATE_SHORTCUT), 0)) {
-            if (packageUser == null || packageUser.mPackageName
-                    .equals(info.activityInfo.packageName)) {
-                result.add(new ShortcutConfigActivityInfoVL(info.activityInfo));
-            }
-        }
-        return result;
-    }
-
-    @Override
-    public List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions() {
-        return mContext.getPackageManager().getPackageInstaller().getAllSessions();
-    }
-
-    @Override
-    public void registerSessionCallback(LooperExecutor executor, SessionCallback sessionCallback) {
-        mContext.getPackageManager().getPackageInstaller().registerSessionCallback(sessionCallback,
-                executor.getHandler());
-    }
-
-    @Override
-    public void unregisterSessionCallback(SessionCallback sessionCallback) {
-        mContext.getPackageManager().getPackageInstaller()
-                .unregisterSessionCallback(sessionCallback);
-    }
-}
-
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java
deleted file mode 100644
index 5e13d00..0000000
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java
+++ /dev/null
@@ -1,156 +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.compat;
-
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.annotation.TargetApi;
-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.content.pm.LauncherApps.PinItemRequest;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.os.Build;
-import android.os.Parcelable;
-import android.os.Process;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.util.PackageUserKey;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@TargetApi(26)
-public class LauncherAppsCompatVO extends LauncherAppsCompatVL {
-
-    LauncherAppsCompatVO(Context context) {
-        super(context);
-    }
-
-    @Override
-    public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user) {
-        try {
-            ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
-            return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
-                    ? null : info;
-        } catch (PackageManager.NameNotFoundException e) {
-            return null;
-        }
-    }
-
-    @Override
-    public List<ShortcutConfigActivityInfo> getCustomShortcutActivityList(
-            @Nullable PackageUserKey packageUser) {
-        List<ShortcutConfigActivityInfo> result = new ArrayList<>();
-        UserHandle myUser = Process.myUserHandle();
-
-        final List<UserHandle> users;
-        final String packageName;
-        if (packageUser == null) {
-            users = UserManagerCompat.getInstance(mContext).getUserProfiles();
-            packageName = null;
-        } else {
-            users = new ArrayList<>(1);
-            users.add(packageUser.mUser);
-            packageName = packageUser.mPackageName;
-        }
-        for (UserHandle user : users) {
-            boolean ignoreTargetSdk = myUser.equals(user);
-            List<LauncherActivityInfo> activities =
-                    mLauncherApps.getShortcutConfigActivityList(packageName, user);
-            for (LauncherActivityInfo activityInfo : activities) {
-                if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion >=
-                        Build.VERSION_CODES.O) {
-                    result.add(new ShortcutConfigActivityInfoVO(activityInfo));
-                }
-            }
-        }
-
-        return result;
-    }
-
-    /**
-     * request.accept() will initiate the following flow:
-     *      -> go-to-system-process for actual processing (a)
-     *      -> callback-to-launcher on UI thread (b)
-     *      -> post callback on the worker thread (c)
-     *      -> Update model and unpin (in system) any shortcut not in out model. (d)
-     *
-     * Note that (b) will take at-least one frame as it involves posting callback from binder
-     * thread to UI thread.
-     * If (d) happens before we add this shortcut to our model, we will end up unpinning
-     * the shortcut in the system.
-     * Here its the caller's responsibility to add the newly created WorkspaceItemInfo immediately
-     * to the model (which may involves a single post-to-worker-thread). That will guarantee
-     * that (d) happens after model is updated.
-     */
-    @Nullable
-    public static WorkspaceItemInfo createWorkspaceItemFromPinItemRequest(
-            Context context, final PinItemRequest request, final long acceptDelay) {
-        if (request != null &&
-                request.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT &&
-                request.isValid()) {
-
-            if (acceptDelay <= 0) {
-                if (!request.accept()) {
-                    return null;
-                }
-            } else {
-                // Block the worker thread until the accept() is called.
-                MODEL_EXECUTOR.execute(new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            Thread.sleep(acceptDelay);
-                        } catch (InterruptedException e) {
-                            // Ignore
-                        }
-                        if (request.isValid()) {
-                            request.accept();
-                        }
-                    }
-                });
-            }
-
-            ShortcutInfo si = request.getShortcutInfo();
-            WorkspaceItemInfo info = new WorkspaceItemInfo(si, context);
-            // Apply the unbadged icon and fetch the actual icon asynchronously.
-            LauncherIcons li = LauncherIcons.obtain(context);
-            info.applyFrom(li.createShortcutIcon(si, false /* badged */));
-            li.recycle();
-            LauncherAppState.getInstance(context).getModel()
-                    .updateAndBindWorkspaceItem(info, si);
-            return info;
-        } else {
-            return null;
-        }
-    }
-
-    public static PinItemRequest getPinItemRequest(Intent intent) {
-        Parcelable extra = intent.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST);
-        return extra instanceof PinItemRequest ? (PinItemRequest) extra : null;
-    }
-}
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java b/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java
deleted file mode 100644
index 48805af..0000000
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.compat;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionCallback;
-
-import com.android.launcher3.util.LooperExecutor;
-
-import java.util.List;
-
-@TargetApi(29)
-public class LauncherAppsCompatVQ extends LauncherAppsCompatVO {
-
-    LauncherAppsCompatVQ(Context context) {
-        super(context);
-    }
-
-    public List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions() {
-        return mLauncherApps.getAllPackageInstallerSessions();
-    }
-
-    @Override
-    public void registerSessionCallback(LooperExecutor executor, SessionCallback sessionCallback) {
-        mLauncherApps.registerPackageInstallerSessionCallback(executor, sessionCallback);
-    }
-
-    @Override
-    public void unregisterSessionCallback(SessionCallback sessionCallback) {
-        mLauncherApps.unregisterPackageInstallerSessionCallback(sessionCallback);
-    }
-}
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java
deleted file mode 100644
index 55df98b..0000000
--- a/src/com/android/launcher3/compat/PackageInstallerCompat.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionInfo;
-import android.os.Process;
-import android.os.UserHandle;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.PackageUserKey;
-
-public abstract class PackageInstallerCompat {
-
-    // Set<String> of session ids of promise icons that have been added to the home screen
-    // as FLAG_PROMISE_NEW_INSTALLS.
-    protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
-
-    public static final int STATUS_INSTALLED = 0;
-    public static final int STATUS_INSTALLING = 1;
-    public static final int STATUS_FAILED = 2;
-
-    private static final Object sInstanceLock = new Object();
-    private static PackageInstallerCompat sInstance;
-
-    public static PackageInstallerCompat getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                sInstance = new PackageInstallerCompatVL(context);
-            }
-            return sInstance;
-        }
-    }
-
-    public static UserHandle getUserHandle(SessionInfo info) {
-        return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle();
-    }
-
-    /**
-     * @return a map of active installs to their progress
-     */
-    public abstract HashMap<PackageUserKey, SessionInfo> updateAndGetActiveSessionCache();
-
-    /**
-     * @return an active SessionInfo for {@param pkg} or null if none exists.
-     */
-    public abstract SessionInfo getActiveSessionInfo(UserHandle user, String pkg);
-
-    public abstract void onStop();
-
-    public static final class PackageInstallInfo {
-        public final ComponentName componentName;
-        public final String packageName;
-        public final int state;
-        public final int progress;
-        public final UserHandle user;
-
-        private PackageInstallInfo(@NonNull SessionInfo info) {
-            this.state = STATUS_INSTALLING;
-            this.packageName = info.getAppPackageName();
-            this.componentName = new ComponentName(packageName, "");
-            this.progress = (int) (info.getProgress() * 100f);
-            this.user = getUserHandle(info);
-        }
-
-        public PackageInstallInfo(String packageName, int state, int progress, UserHandle user) {
-            this.state = state;
-            this.packageName = packageName;
-            this.componentName = new ComponentName(packageName, "");
-            this.progress = progress;
-            this.user = user;
-        }
-
-        public static PackageInstallInfo fromInstallingState(SessionInfo info) {
-            return new PackageInstallInfo(info);
-        }
-
-        public static PackageInstallInfo fromState(int state, String packageName, UserHandle user) {
-            return new PackageInstallInfo(packageName, state, 0 /* progress */, user);
-        }
-
-    }
-
-    public abstract List<SessionInfo> getAllVerifiedSessions();
-
-    /**
-     * Returns true if a promise icon was already added to the home screen for {@param sessionId}.
-     * Applicable only for icons with flag FLAG_PROMISE_NEW_INSTALLS.
-     */
-    public abstract boolean promiseIconAddedForId(int sessionId);
-
-    /**
-     * Applicable only for icons with flag FLAG_PROMISE_NEW_INSTALLS.
-     */
-    public abstract void removePromiseIconId(int sessionId);
-}
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
deleted file mode 100644
index 409b21d..0000000
--- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionCallback;
-import android.content.pm.PackageInstaller.SessionInfo;
-import android.content.pm.PackageManager;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.SparseArray;
-
-import com.android.launcher3.SessionCommitReceiver;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Thunk;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
-import static com.android.launcher3.Utilities.getPrefs;
-
-public class PackageInstallerCompatVL extends PackageInstallerCompat {
-
-    private static final boolean DEBUG = false;
-
-    @Thunk final SparseArray<PackageUserKey> mActiveSessions = new SparseArray<>();
-
-    @Thunk final PackageInstaller mInstaller;
-    private final IconCache mCache;
-    private final Context mAppContext;
-    private final HashMap<String,Boolean> mSessionVerifiedMap = new HashMap<>();
-    private final LauncherAppsCompat mLauncherApps;
-    private final IntSet mPromiseIconIds;
-
-    PackageInstallerCompatVL(Context context) {
-        mAppContext = context.getApplicationContext();
-        mInstaller = context.getPackageManager().getPackageInstaller();
-        mCache = LauncherAppState.getInstance(context).getIconCache();
-        mLauncherApps = LauncherAppsCompat.getInstance(context);
-        mLauncherApps.registerSessionCallback(MODEL_EXECUTOR, mCallback);
-        mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
-                getPrefs(context).getString(PROMISE_ICON_IDS, "")));
-
-        cleanUpPromiseIconIds();
-    }
-
-    private void cleanUpPromiseIconIds() {
-        IntArray existingIds = new IntArray();
-        for (SessionInfo info : updateAndGetActiveSessionCache().values()) {
-            existingIds.add(info.getSessionId());
-        }
-        IntArray idsToRemove = new IntArray();
-
-        for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) {
-            if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) {
-                idsToRemove.add(mPromiseIconIds.getArray().get(i));
-            }
-        }
-        for (int i = idsToRemove.size() - 1; i >= 0; --i) {
-            mPromiseIconIds.getArray().removeValue(idsToRemove.get(i));
-        }
-    }
-
-    @Override
-    public HashMap<PackageUserKey, SessionInfo> updateAndGetActiveSessionCache() {
-        HashMap<PackageUserKey, SessionInfo> activePackages = new HashMap<>();
-        for (SessionInfo info : getAllVerifiedSessions()) {
-            addSessionInfoToCache(info, getUserHandle(info));
-            if (info.getAppPackageName() != null) {
-                activePackages.put(new PackageUserKey(info.getAppPackageName(),
-                        getUserHandle(info)), info);
-                mActiveSessions.put(info.getSessionId(),
-                        new PackageUserKey(info.getAppPackageName(), getUserHandle(info)));
-            }
-        }
-        return activePackages;
-    }
-
-    public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) {
-        for (SessionInfo info : getAllVerifiedSessions()) {
-            boolean match = pkg.equals(info.getAppPackageName());
-            if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) {
-                match = false;
-            }
-            if (match) {
-                return info;
-            }
-        }
-        return null;
-    }
-
-    @Thunk void addSessionInfoToCache(SessionInfo info, UserHandle user) {
-        String packageName = info.getAppPackageName();
-        if (packageName != null) {
-            mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(),
-                    info.getAppLabel());
-        }
-    }
-
-    @Override
-    public void onStop() {
-        mLauncherApps.unregisterSessionCallback(mCallback);
-    }
-
-    @Thunk void sendUpdate(PackageInstallInfo info) {
-        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-        if (app != null) {
-            app.getModel().setPackageState(info);
-        }
-    }
-
-    /**
-     * Add a promise app icon to the workspace iff:
-     * - The settings for it are enabled
-     * - The user installed the app
-     * - There is an app icon and label (For apps with no launching activity, no icon is provided).
-     * - The app is not already installed
-     * - A promise icon for the session has not already been created
-     */
-    private void tryQueuePromiseAppIcon(SessionInfo sessionInfo) {
-        if (Utilities.ATLEAST_OREO && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
-                && SessionCommitReceiver.isEnabled(mAppContext)
-                && verify(sessionInfo) != null
-                && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
-                && sessionInfo.getAppIcon() != null
-                && !TextUtils.isEmpty(sessionInfo.getAppLabel())
-                && !mPromiseIconIds.contains(sessionInfo.getSessionId())
-                && mLauncherApps.getApplicationInfo(sessionInfo.getAppPackageName(), 0,
-                        getUserHandle(sessionInfo)) == null) {
-            SessionCommitReceiver.queuePromiseAppIconAddition(mAppContext, sessionInfo);
-            mPromiseIconIds.add(sessionInfo.getSessionId());
-            updatePromiseIconPrefs();
-        }
-    }
-
-    private final SessionCallback mCallback = new SessionCallback() {
-
-        @Override
-        public void onCreated(int sessionId) {
-            SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
-            if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS && sessionInfo != null) {
-                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-                if (app != null) {
-                    app.getModel().onInstallSessionCreated(
-                            PackageInstallInfo.fromInstallingState(sessionInfo));
-                }
-            }
-
-            tryQueuePromiseAppIcon(sessionInfo);
-        }
-
-        @Override
-        public void onFinished(int sessionId, boolean success) {
-            // For a finished session, we can't get the session info. So use the
-            // packageName from our local cache.
-            PackageUserKey key = mActiveSessions.get(sessionId);
-            mActiveSessions.remove(sessionId);
-
-            if (key != null && key.mPackageName != null) {
-                String packageName = key.mPackageName;
-                sendUpdate(PackageInstallInfo.fromState(success ? STATUS_INSTALLED : STATUS_FAILED,
-                        packageName, key.mUser));
-
-                if (!success && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
-                        && mPromiseIconIds.contains(sessionId)) {
-                    LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
-                    if (appState != null) {
-                        appState.getModel().onSessionFailure(packageName, key.mUser);
-                    }
-                    // If it is successful, the id is removed in the the package added flow.
-                    removePromiseIconId(sessionId);
-                }
-            }
-        }
-
-        @Override
-        public void onProgressChanged(int sessionId, float progress) {
-            SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
-            if (session != null && session.getAppPackageName() != null) {
-                sendUpdate(PackageInstallInfo.fromInstallingState(session));
-            }
-        }
-
-        @Override
-        public void onActiveChanged(int sessionId, boolean active) { }
-
-        @Override
-        public void onBadgingChanged(int sessionId) {
-            SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
-            if (sessionInfo != null) {
-                tryQueuePromiseAppIcon(sessionInfo);
-            }
-        }
-
-        private SessionInfo pushSessionDisplayToLauncher(int sessionId) {
-            SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
-            if (session != null && session.getAppPackageName() != null) {
-                UserHandle user = getUserHandle(session);
-                mActiveSessions.put(session.getSessionId(),
-                        new PackageUserKey(session.getAppPackageName(), user));
-                addSessionInfoToCache(session, user);
-                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-                if (app != null) {
-                    app.getModel().updateSessionDisplayInfo(session.getAppPackageName(),
-                            user);
-                }
-                return session;
-            }
-            return null;
-        }
-    };
-
-    private PackageInstaller.SessionInfo verify(PackageInstaller.SessionInfo sessionInfo) {
-        if (sessionInfo == null
-                || sessionInfo.getInstallerPackageName() == null
-                || TextUtils.isEmpty(sessionInfo.getAppPackageName())) {
-            return null;
-        }
-        String pkg = sessionInfo.getInstallerPackageName();
-        synchronized (mSessionVerifiedMap) {
-            if (!mSessionVerifiedMap.containsKey(pkg)) {
-                LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mAppContext);
-                boolean hasSystemFlag = launcherApps.getApplicationInfo(pkg,
-                        ApplicationInfo.FLAG_SYSTEM, getUserHandle(sessionInfo)) != null;
-                mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag);
-            }
-        }
-        return mSessionVerifiedMap.get(pkg) ? sessionInfo : null;
-    }
-
-    @Override
-    public List<SessionInfo> getAllVerifiedSessions() {
-        List<SessionInfo> list = new ArrayList<>(Utilities.ATLEAST_Q
-                ? mLauncherApps.getAllPackageInstallerSessions()
-                : mInstaller.getAllSessions());
-        Iterator<SessionInfo> it = list.iterator();
-        while (it.hasNext()) {
-            if (verify(it.next()) == null) {
-                it.remove();
-            }
-        }
-        return list;
-    }
-
-    @Override
-    public boolean promiseIconAddedForId(int sessionId) {
-        return mPromiseIconIds.contains(sessionId);
-    }
-
-    @Override
-    public void removePromiseIconId(int sessionId) {
-        if (mPromiseIconIds.contains(sessionId)) {
-            mPromiseIconIds.getArray().removeValue(sessionId);
-            updatePromiseIconPrefs();
-        }
-    }
-
-    private void updatePromiseIconPrefs() {
-        getPrefs(mAppContext).edit()
-                .putString(PROMISE_ICON_IDS, mPromiseIconIds.getArray().toConcatString())
-                .apply();
-    }
-}
diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java
deleted file mode 100644
index 2c0088e..0000000
--- a/src/com/android/launcher3/compat/UserManagerCompat.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2014 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.compat;
-
-import android.content.Context;
-import android.os.UserHandle;
-
-import com.android.launcher3.Utilities;
-
-import java.util.List;
-
-public abstract class UserManagerCompat {
-    protected UserManagerCompat() {
-    }
-
-    private static final Object sInstanceLock = new Object();
-    private static UserManagerCompat sInstance;
-
-    public static UserManagerCompat getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                if (Utilities.ATLEAST_P) {
-                    sInstance = new UserManagerCompatVP(context.getApplicationContext());
-                } else {
-                    sInstance = new UserManagerCompatVNMr1(context.getApplicationContext());
-                }
-            }
-            return sInstance;
-        }
-    }
-
-    /**
-     * Creates a cache for users.
-     */
-    public abstract void enableAndResetCache();
-
-    public abstract List<UserHandle> getUserProfiles();
-    public abstract long getSerialNumberForUser(UserHandle user);
-    public abstract UserHandle getUserForSerialNumber(long serialNumber);
-    public abstract boolean isQuietModeEnabled(UserHandle user);
-    public abstract boolean isUserUnlocked(UserHandle user);
-
-    public abstract boolean isDemoUser();
-    public abstract boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user);
-    public abstract boolean isAnyProfileQuietModeEnabled();
-
-    public abstract boolean hasWorkProfile();
-}
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVNMr1.java b/src/com/android/launcher3/compat/UserManagerCompatVNMr1.java
deleted file mode 100644
index 18d9053..0000000
--- a/src/com/android/launcher3/compat/UserManagerCompatVNMr1.java
+++ /dev/null
@@ -1,138 +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.compat;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.ArrayMap;
-import android.util.LongSparseArray;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@TargetApi(Build.VERSION_CODES.N_MR1)
-public class UserManagerCompatVNMr1 extends UserManagerCompat {
-
-    protected final UserManager mUserManager;
-
-    protected LongSparseArray<UserHandle> mUsers;
-    // Create a separate reverse map as LongSparseArray.indexOfValue checks if objects are same
-    // and not {@link Object#equals}
-    protected ArrayMap<UserHandle, Long> mUserToSerialMap;
-
-    UserManagerCompatVNMr1(Context context) {
-        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-    }
-
-    @Override
-    public boolean isQuietModeEnabled(UserHandle user) {
-        return mUserManager.isQuietModeEnabled(user);
-    }
-
-    @Override
-    public boolean isUserUnlocked(UserHandle user) {
-        return mUserManager.isUserUnlocked(user);
-    }
-
-    @Override
-    public boolean isAnyProfileQuietModeEnabled() {
-        List<UserHandle> userProfiles = getUserProfiles();
-        for (UserHandle userProfile : userProfiles) {
-            if (Process.myUserHandle().equals(userProfile)) {
-                continue;
-            }
-            if (isQuietModeEnabled(userProfile)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public long getSerialNumberForUser(UserHandle user) {
-        synchronized (this) {
-            if (mUserToSerialMap != null) {
-                Long serial = mUserToSerialMap.get(user);
-                return serial == null ? 0 : serial;
-            }
-        }
-        return mUserManager.getSerialNumberForUser(user);
-    }
-
-    @Override
-    public UserHandle getUserForSerialNumber(long serialNumber) {
-        synchronized (this) {
-            if (mUsers != null) {
-                return mUsers.get(serialNumber);
-            }
-        }
-        return mUserManager.getUserForSerialNumber(serialNumber);
-    }
-
-    @Override
-    public boolean isDemoUser() {
-        return mUserManager.isDemoUser();
-    }
-
-    @Override
-    public boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
-        return false;
-    }
-
-    @Override
-    public void enableAndResetCache() {
-        synchronized (this) {
-            mUsers = new LongSparseArray<>();
-            mUserToSerialMap = new ArrayMap<>();
-            List<UserHandle> users = mUserManager.getUserProfiles();
-            if (users != null) {
-                for (UserHandle user : users) {
-                    long serial = mUserManager.getSerialNumberForUser(user);
-                    mUsers.put(serial, user);
-                    mUserToSerialMap.put(user, serial);
-                }
-            }
-        }
-    }
-
-    @Override
-    public List<UserHandle> getUserProfiles() {
-        synchronized (this) {
-            if (mUsers != null) {
-                return new ArrayList<>(mUserToSerialMap.keySet());
-            }
-        }
-
-        List<UserHandle> users = mUserManager.getUserProfiles();
-        return users == null ? Collections.<UserHandle>emptyList() : users;
-    }
-
-    @Override
-    public boolean hasWorkProfile() {
-        synchronized (this) {
-            if (mUsers != null) {
-                return mUsers.size() > 1;
-            }
-        }
-        return getUserProfiles().size() > 1;
-    }
-}
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVP.java b/src/com/android/launcher3/compat/UserManagerCompatVP.java
deleted file mode 100644
index fa3902b..0000000
--- a/src/com/android/launcher3/compat/UserManagerCompatVP.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.compat;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.UserHandle;
-
-@TargetApi(Build.VERSION_CODES.P)
-public class UserManagerCompatVP extends UserManagerCompatVNMr1 {
-
-    UserManagerCompatVP(Context context) {
-        super(context);
-    }
-
-    @Override
-    public boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
-        return mUserManager.requestQuietModeEnabled(enableQuietMode, user);
-    }
-}
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
deleted file mode 100644
index 4b90e42..0000000
--- a/src/com/android/launcher3/config/BaseFlags.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.config;
-
-import static androidx.core.util.Preconditions.checkNotNull;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.Keep;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.TogglableFlag;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * Defines a set of flags used to control various launcher behaviors.
- *
- * <p>All the flags should be defined here with appropriate default values.
- */
-@Keep
-public abstract class BaseFlags {
-
-    private static final Object sLock = new Object();
-    @GuardedBy("sLock")
-    private static final List<TogglableFlag> sFlags = new ArrayList<>();
-
-    static final String FLAGS_PREF_NAME = "featureFlags";
-
-    BaseFlags() {
-        throw new UnsupportedOperationException("Don't instantiate BaseFlags");
-    }
-
-    public static boolean showFlagTogglerUi(Context context) {
-        return Utilities.IS_DEBUG_DEVICE && Utilities.isDevelopersOptionsEnabled(context);
-    }
-
-    public static final boolean IS_DOGFOOD_BUILD = false;
-
-    // When enabled the promise icon is visible in all apps while installation an app.
-    public static final boolean LAUNCHER3_PROMISE_APPS_IN_ALL_APPS = false;
-
-    // When enabled a promise icon is added to the home screen when install session is active.
-    public static final TogglableFlag PROMISE_APPS_NEW_INSTALLS =
-            new TogglableFlag("PROMISE_APPS_NEW_INSTALLS", true,
-                    "Adds a promise icon to the home screen for new install sessions.");
-
-    // Enable moving the QSB on the 0th screen of the workspace
-    public static final boolean QSB_ON_FIRST_SCREEN = true;
-
-    public static final TogglableFlag EXAMPLE_FLAG = new TogglableFlag("EXAMPLE_FLAG", true,
-            "An example flag that doesn't do anything. Useful for testing");
-
-    //Feature flag to enable pulling down navigation shade from workspace.
-    public static final boolean PULL_DOWN_STATUS_BAR = true;
-
-    // Features to control Launcher3Go behavior
-    public static final boolean GO_DISABLE_WIDGETS = false;
-
-    // When enabled shows a work profile tab in all apps
-    public static final boolean ALL_APPS_TABS_ENABLED = true;
-
-    // When true, overview shows screenshots in the orientation they were taken rather than
-    // trying to make them fit the orientation the device is in.
-    public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true;
-
-    /**
-     * Feature flag to handle define config changes dynamically instead of killing the process.
-     */
-    public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
-            "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
-
-    public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
-            false, "Enable springs for quickstep animations");
-
-    public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
-            "ADAPTIVE_ICON_WINDOW_ANIM", true,
-            "Use adaptive icons for window animations.");
-
-    public static final TogglableFlag ENABLE_QUICKSTEP_LIVE_TILE = new TogglableFlag(
-            "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
-
-    public static final TogglableFlag ENABLE_HINTS_IN_OVERVIEW = new TogglableFlag(
-            "ENABLE_HINTS_IN_OVERVIEW", true,
-            "Show chip hints and gleams on the overview screen");
-
-    public static final TogglableFlag FAKE_LANDSCAPE_UI = new TogglableFlag(
-            "FAKE_LANDSCAPE_UI", false,
-            "Rotate launcher UI instead of using transposed layout");
-
-    public static final TogglableFlag FOLDER_NAME_SUGGEST = new TogglableFlag(
-            "FOLDER_NAME_SUGGEST", true,
-            "Suggests folder names instead of blank text.");
-
-    public static final TogglableFlag APP_SEARCH_IMPROVEMENTS = new TogglableFlag(
-            "APP_SEARCH_IMPROVEMENTS", true,
-            "Adds localized title and keyword search and ranking");
-
-    public static final TogglableFlag ENABLE_PREDICTION_DISMISS = new TogglableFlag(
-            "ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
-
-    public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
-            "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
-            "Allow Launcher to handle nav bar gestures while Assistant is running over it");
-
-    public static void initialize(Context context) {
-        // Avoid the disk read for user builds
-        if (Utilities.IS_DEBUG_DEVICE) {
-            synchronized (sLock) {
-                for (BaseTogglableFlag flag : sFlags) {
-                    flag.initialize(context);
-                }
-            }
-        }
-        APP_SEARCH_IMPROVEMENTS.initialize(context);
-    }
-
-    static List<TogglableFlag> getTogglableFlags() {
-        // By Java Language Spec 12.4.2
-        // https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2, the
-        // TogglableFlag instances on BaseFlags will be created before those on the FeatureFlags
-        // subclass. This code handles flags that are redeclared in FeatureFlags, ensuring the
-        // FeatureFlags one takes priority.
-        SortedMap<String, TogglableFlag> flagsByKey = new TreeMap<>();
-        synchronized (sLock) {
-            for (TogglableFlag flag : sFlags) {
-                flagsByKey.put(((BaseTogglableFlag) flag).getKey(), flag);
-            }
-        }
-        return new ArrayList<>(flagsByKey.values());
-    }
-
-    public static abstract class BaseTogglableFlag {
-        private final String key;
-        // should be value that is hardcoded in client side.
-        // Comparatively, getDefaultValue() can be overridden.
-        private final boolean defaultValue;
-        private final String description;
-        private boolean currentValue;
-
-        public BaseTogglableFlag(
-                String key,
-                boolean defaultValue,
-                String description) {
-            this.key = checkNotNull(key);
-            this.currentValue = this.defaultValue = defaultValue;
-            this.description = checkNotNull(description);
-
-            synchronized (sLock) {
-                sFlags.add((TogglableFlag)this);
-            }
-        }
-
-        /** Set the value of this flag. This should only be used in tests. */
-        @VisibleForTesting
-        void setForTests(boolean value) {
-            currentValue = value;
-        }
-
-        public String getKey() {
-            return key;
-        }
-
-        protected void initialize(Context context) {
-            currentValue = getFromStorage(context, getDefaultValue());
-        }
-
-        protected abstract boolean getOverridenDefaultValue(boolean value);
-
-        protected abstract void addChangeListener(Context context, Runnable r);
-
-        public void updateStorage(Context context, boolean value) {
-            SharedPreferences.Editor editor = context.getSharedPreferences(FLAGS_PREF_NAME,
-                    Context.MODE_PRIVATE).edit();
-            if (value == getDefaultValue()) {
-                editor.remove(key).apply();
-            } else {
-                editor.putBoolean(key, value).apply();
-            }
-        }
-
-        boolean getFromStorage(Context context, boolean defaultValue) {
-            return context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
-                    .getBoolean(key, getDefaultValue());
-        }
-
-        boolean getDefaultValue() {
-            return getOverridenDefaultValue(defaultValue);
-        }
-
-        /** Returns the value of the flag at process start, including any overrides present. */
-        public boolean get() {
-            return currentValue;
-        }
-
-        String getDescription() {
-            return description;
-        }
-
-        @Override
-        public String toString() {
-            return "TogglableFlag{"
-                    + "key=" + key + ", "
-                    + "defaultValue=" + defaultValue + ", "
-                    + "overriddenDefaultValue=" + getOverridenDefaultValue(defaultValue) + ", "
-                    + "currentValue=" + currentValue + ", "
-                    + "description=" + description
-                    + "}";
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (o == this) {
-                return true;
-            }
-            if (o instanceof TogglableFlag) {
-                BaseTogglableFlag that = (BaseTogglableFlag) o;
-                return (this.key.equals(that.getKey()))
-                        && (this.getDefaultValue() == that.getDefaultValue())
-                        && (this.description.equals(that.getDescription()));
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            int h$ = 1;
-            h$ *= 1000003;
-            h$ ^= key.hashCode();
-            h$ *= 1000003;
-            h$ ^= getDefaultValue() ? 1231 : 1237;
-            h$ *= 1000003;
-            h$ ^= description.hashCode();
-            return h$;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
new file mode 100644
index 0000000..5c4a492
--- /dev/null
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -0,0 +1,275 @@
+/*
+ * 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.config;
+
+import android.content.Context;
+
+import com.android.launcher3.BuildConfig;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.uioverrides.DeviceFlag;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines a set of flags used to control various launcher behaviors.
+ *
+ * <p>All the flags should be defined here with appropriate default values.
+ */
+public final class FeatureFlags {
+
+    private static final List<DebugFlag> sDebugFlags = new ArrayList<>();
+
+    public static final String FLAGS_PREF_NAME = "featureFlags";
+
+    private FeatureFlags() { }
+
+    public static boolean showFlagTogglerUi(Context context) {
+        return Utilities.IS_DEBUG_DEVICE && Utilities.isDevelopersOptionsEnabled(context);
+    }
+
+    /**
+     * True when the build has come from Android Studio and is being used for local debugging.
+     */
+    public static final boolean IS_STUDIO_BUILD = BuildConfig.DEBUG;
+
+    /**
+     * Enable moving the QSB on the 0th screen of the workspace. This is not a configuration feature
+     * and should be modified at a project level.
+     */
+    public static final boolean QSB_ON_FIRST_SCREEN = true;
+
+    /**
+     * Feature flag to handle define config changes dynamically instead of killing the process.
+     *
+     *
+     * To add a new flag that can be toggled through the flags UI:
+     *
+     * Declare a new ToggleableFlag below. Give it a unique key (e.g. "QSB_ON_FIRST_SCREEN"),
+     *    and set a default value for the flag. This will be the default value on Debug builds.
+     */
+    // When enabled the promise icon is visible in all apps while installation an app.
+    public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag(
+            "PROMISE_APPS_IN_ALL_APPS", false, "Add promise icon in all-apps");
+
+    // When enabled a promise icon is added to the home screen when install session is active.
+    public static final BooleanFlag PROMISE_APPS_NEW_INSTALLS = getDebugFlag(
+            "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");
+
+    public static final BooleanFlag UNSTABLE_SPRINGS = getDebugFlag(
+            "UNSTABLE_SPRINGS", false, "Enable unstable springs for quickstep animations");
+
+    public static final BooleanFlag KEYGUARD_ANIMATION = getDebugFlag(
+            "KEYGUARD_ANIMATION", false, "Enable animation for keyguard going away on wallpaper");
+
+    public static final BooleanFlag ADAPTIVE_ICON_WINDOW_ANIM = getDebugFlag(
+            "ADAPTIVE_ICON_WINDOW_ANIM", true, "Use adaptive icons for window animations.");
+
+    public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag(
+            "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
+
+    // Keep as DeviceFlag to allow remote disable in emergency.
+    public static final BooleanFlag ENABLE_SUGGESTED_ACTIONS_OVERVIEW = new DeviceFlag(
+            "ENABLE_SUGGESTED_ACTIONS_OVERVIEW", false, "Show chip hints on the overview screen");
+
+    public static final BooleanFlag FOLDER_NAME_SUGGEST = new DeviceFlag(
+            "FOLDER_NAME_SUGGEST", true,
+            "Suggests folder names instead of blank text.");
+
+    public static final BooleanFlag FOLDER_NAME_MAJORITY_RANKING = getDebugFlag(
+            "FOLDER_NAME_MAJORITY_RANKING", true,
+            "Suggests folder names based on majority based ranking.");
+
+    public static final BooleanFlag APP_SEARCH_IMPROVEMENTS = new DeviceFlag(
+            "APP_SEARCH_IMPROVEMENTS", true,
+            "Adds localized title and keyword search and ranking");
+
+    public static final BooleanFlag ENABLE_PREDICTION_DISMISS = getDebugFlag(
+            "ENABLE_PREDICTION_DISMISS", true, "Allow option to dimiss apps from predicted list");
+
+    public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
+            "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
+
+    public static final BooleanFlag ENABLE_QUICK_CAPTURE_WINDOW = getDebugFlag(
+            "ENABLE_QUICK_CAPTURE_WINDOW", false, "Use window to host quick capture");
+
+    public static final BooleanFlag FORCE_LOCAL_OVERSCROLL_PLUGIN = getDebugFlag(
+            "FORCE_LOCAL_OVERSCROLL_PLUGIN", false,
+            "Use a launcher-provided OverscrollPlugin if available");
+
+    public static final BooleanFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = getDebugFlag(
+            "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");
+
+    public static final BooleanFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = getDebugFlag(
+            "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
+
+    public static final BooleanFlag MULTI_DB_GRID_MIRATION_ALGO = getDebugFlag(
+            "MULTI_DB_GRID_MIRATION_ALGO", true, "Use the multi-db grid migration algorithm");
+
+    public static final BooleanFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = getDebugFlag(
+            "ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", true, "Show launcher preview in grid picker");
+
+    public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
+            "ENABLE_OVERVIEW_ACTIONS", true, "Show app actions instead of the shelf in Overview."
+            + " As part of this decoupling, also distinguish swipe up from nav bar vs above it.");
+
+    // Keep as DeviceFlag for remote disable in emergency.
+    public static final BooleanFlag ENABLE_OVERVIEW_SELECTIONS = new DeviceFlag(
+            "ENABLE_OVERVIEW_SELECTIONS", true, "Show Select Mode button in Overview Actions");
+
+    public static final BooleanFlag ENABLE_OVERVIEW_SHARE = getDebugFlag(
+            "ENABLE_OVERVIEW_SHARE", false, "Show Share button in Overview Actions");
+
+    public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag(
+            "ENABLE_DATABASE_RESTORE", true,
+            "Enable database restore when new restore session is created");
+
+    public static final BooleanFlag ENABLE_UNIVERSAL_SMARTSPACE = getDebugFlag(
+            "ENABLE_UNIVERSAL_SMARTSPACE", false,
+            "Replace Smartspace with a version rendered by System UI.");
+
+    public static final BooleanFlag ENABLE_LSQ_VELOCITY_PROVIDER = getDebugFlag(
+            "ENABLE_LSQ_VELOCITY_PROVIDER", true,
+            "Use Least Square 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,
+            "Always use hardware optimization for folder animations.");
+
+    public static final BooleanFlag ENABLE_ALL_APPS_EDU = getDebugFlag(
+            "ENABLE_ALL_APPS_EDU", true,
+            "Shows user a tutorial on how to get to All Apps after X amount of attempts.");
+
+    public static final BooleanFlag SEPARATE_RECENTS_ACTIVITY = getDebugFlag(
+            "SEPARATE_RECENTS_ACTIVITY", false,
+            "Uses a separate recents activity instead of using the integrated recents+Launcher UI");
+
+    public static final BooleanFlag USER_EVENT_DISPATCHER = new DeviceFlag(
+            "USER_EVENT_DISPATCHER", true, "User event dispatcher collects logs.");
+
+    public static void initialize(Context context) {
+        synchronized (sDebugFlags) {
+            for (DebugFlag flag : sDebugFlags) {
+                flag.initialize(context);
+            }
+            sDebugFlags.sort((f1, f2) -> f1.key.compareToIgnoreCase(f2.key));
+        }
+    }
+
+    static List<DebugFlag> getDebugFlags() {
+        synchronized (sDebugFlags) {
+            return new ArrayList<>(sDebugFlags);
+        }
+    }
+
+    public static void dump(PrintWriter pw) {
+        pw.println("DeviceFlags:");
+        synchronized (sDebugFlags) {
+            for (DebugFlag flag : sDebugFlags) {
+                if (flag instanceof DeviceFlag) {
+                    pw.println("  " + flag.toString());
+                }
+            }
+        }
+        pw.println("DebugFlags:");
+        synchronized (sDebugFlags) {
+            for (DebugFlag flag : sDebugFlags) {
+                if (!(flag instanceof DeviceFlag)) {
+                    pw.println("  " + flag.toString());
+                }
+            }
+        }
+    }
+
+    public static class BooleanFlag {
+
+        public final String key;
+        public boolean defaultValue;
+
+        public BooleanFlag(String key, boolean defaultValue) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+        }
+
+        public boolean get() {
+            return defaultValue;
+        }
+
+        @Override
+        public String toString() {
+            return appendProps(new StringBuilder()).toString();
+        }
+
+        protected StringBuilder appendProps(StringBuilder src) {
+            return src.append(key).append(", defaultValue=").append(defaultValue);
+        }
+
+        public void addChangeListener(Context context, Runnable r) { }
+    }
+
+    public static class DebugFlag extends BooleanFlag {
+
+        public final String description;
+        private boolean mCurrentValue;
+
+        public DebugFlag(String key, boolean defaultValue, String description) {
+            super(key, defaultValue);
+            this.description = description;
+            mCurrentValue = this.defaultValue;
+            synchronized (sDebugFlags) {
+                sDebugFlags.add(this);
+            }
+        }
+
+        @Override
+        public boolean get() {
+            return mCurrentValue;
+        }
+
+        public void initialize(Context context) {
+            mCurrentValue = context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+                    .getBoolean(key, defaultValue);
+        }
+
+        @Override
+        protected StringBuilder appendProps(StringBuilder src) {
+            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue);
+        }
+    }
+
+    private static BooleanFlag getDebugFlag(String key, boolean defaultValue, String description) {
+        return Utilities.IS_DEBUG_DEVICE
+                ? new DebugFlag(key, defaultValue, description)
+                : new BooleanFlag(key, defaultValue);
+    }
+}
diff --git a/src/com/android/launcher3/config/FlagTogglerPrefUi.java b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
index 54e5322..6729f74 100644
--- a/src/com/android/launcher3/config/FlagTogglerPrefUi.java
+++ b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.config;
 
+import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Process;
@@ -25,14 +27,13 @@
 import android.view.MenuItem;
 import android.widget.Toast;
 
-import com.android.launcher3.R;
-
 import androidx.preference.PreferenceDataStore;
-import androidx.preference.PreferenceFragment;
+import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.SwitchPreference;
-import com.android.launcher3.config.BaseFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
+
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
 
 /**
  * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
@@ -41,7 +42,7 @@
 
     private static final String TAG = "FlagTogglerPrefFrag";
 
-    private final PreferenceFragment mFragment;
+    private final PreferenceFragmentCompat mFragment;
     private final Context mContext;
     private final SharedPreferences mSharedPreferences;
 
@@ -49,34 +50,37 @@
 
         @Override
         public void putBoolean(String key, boolean value) {
-            for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-                if (flag.getKey().equals(key)) {
-                    boolean prevValue = flag.get();
-                    flag.updateStorage(mContext, value);
-                    updateMenu();
-                    if (flag.get() != prevValue) {
-                        Toast.makeText(mContext, "Flag applied", Toast.LENGTH_SHORT).show();
+            for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+                if (flag.key.equals(key)) {
+                    SharedPreferences.Editor editor = mContext.getSharedPreferences(
+                            FLAGS_PREF_NAME, Context.MODE_PRIVATE).edit();
+                    if (value == flag.defaultValue) {
+                        editor.remove(key).apply();
+                    } else {
+                        editor.putBoolean(key, value).apply();
                     }
+                    updateMenu();
                 }
             }
         }
 
         @Override
         public boolean getBoolean(String key, boolean defaultValue) {
-            for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
-                if (flag.getKey().equals(key)) {
-                    return flag.getFromStorage(mContext, defaultValue);
+            for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+                if (flag.key.equals(key)) {
+                    return mContext.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+                            .getBoolean(key, flag.defaultValue);
                 }
             }
             return defaultValue;
         }
     };
 
-    public FlagTogglerPrefUi(PreferenceFragment fragment) {
+    public FlagTogglerPrefUi(PreferenceFragmentCompat fragment) {
         mFragment = fragment;
         mContext = fragment.getActivity();
         mSharedPreferences = mContext.getSharedPreferences(
-                FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+                FLAGS_PREF_NAME, Context.MODE_PRIVATE);
     }
 
     public void applyTo(PreferenceGroup parent) {
@@ -84,12 +88,12 @@
         // flag with a different value than the default. That way, when we flip flags in
         // future, engineers will pick up the new value immediately. To accomplish this, we use a
         // custom preference data store.
-        for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+        for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
             SwitchPreference switchPreference = new SwitchPreference(mContext);
-            switchPreference.setKey(flag.getKey());
-            switchPreference.setDefaultValue(flag.getDefaultValue());
+            switchPreference.setKey(flag.key);
+            switchPreference.setDefaultValue(flag.defaultValue);
             switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
-            switchPreference.setTitle(flag.getKey());
+            switchPreference.setTitle(flag.key);
             updateSummary(switchPreference, flag);
             switchPreference.setPreferenceDataStore(mDataStore);
             parent.addPreference(switchPreference);
@@ -100,11 +104,11 @@
     /**
      * Updates the summary to show the description and whether the flag overrides the default value.
      */
-    private void updateSummary(SwitchPreference switchPreference, BaseTogglableFlag flag) {
-        String onWarning = flag.getDefaultValue() ? "" : "<b>OVERRIDDEN</b><br>";
-        String offWarning = flag.getDefaultValue() ? "<b>OVERRIDDEN</b><br>" : "";
-        switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.getDescription()));
-        switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.getDescription()));
+    private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
+        String onWarning = flag.defaultValue ? "" : "<b>OVERRIDDEN</b><br>";
+        String offWarning = flag.defaultValue ? "<b>OVERRIDDEN</b><br>" : "";
+        switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.description));
+        switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.description));
     }
 
     private void updateMenu() {
@@ -135,12 +139,12 @@
         }
     }
 
-    private boolean getFlagStateFromSharedPrefs(BaseTogglableFlag flag) {
-        return mDataStore.getBoolean(flag.getKey(), flag.getDefaultValue());
+    private boolean getFlagStateFromSharedPrefs(DebugFlag flag) {
+        return mDataStore.getBoolean(flag.key, flag.defaultValue);
     }
 
     private boolean anyChanged() {
-        for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+        for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
             if (getFlagStateFromSharedPrefs(flag) != flag.get()) {
                 return true;
             }
diff --git a/src/com/android/launcher3/dot/DotInfo.java b/src/com/android/launcher3/dot/DotInfo.java
index 4ff0539..fc180d1 100644
--- a/src/com/android/launcher3/dot/DotInfo.java
+++ b/src/com/android/launcher3/dot/DotInfo.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.dot;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 
@@ -83,4 +85,10 @@
     public int getNotificationCount() {
         return Math.min(mTotalCount, MAX_COUNT);
     }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return Integer.toString(mTotalCount);
+    }
 }
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 9fb1090..0df6713 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -45,13 +45,13 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.InstallShortcutReceiver;
 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.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.LauncherAppsCompatVO;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.InstantAppResolver;
@@ -60,6 +60,7 @@
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetImageView;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.function.Supplier;
 
@@ -81,7 +82,7 @@
 
     // Widget request specific options.
     private LauncherAppWidgetHost mAppWidgetHost;
-    private AppWidgetManagerCompat mAppWidgetManager;
+    private WidgetManagerHelper mAppWidgetManager;
     private int mPendingBindWidgetId;
     private Bundle mWidgetOptions;
 
@@ -92,7 +93,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        mRequest = LauncherAppsCompatVO.getPinItemRequest(getIntent());
+        mRequest = PinRequestHelper.getPinItemRequest(getIntent());
         if (mRequest == null) {
             finish();
             return;
@@ -169,16 +170,14 @@
             }
         }, null, View.DRAG_FLAG_GLOBAL);
 
-
-        Intent homeIntent = listener.addToIntent(
-                new Intent(Intent.ACTION_MAIN)
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
                         .addCategory(Intent.CATEGORY_HOME)
                         .setPackage(getPackageName())
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-
-        listener.initWhenReady();
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Launcher.ACTIVITY_TRACKER.runCallbackWhenActivityExists(listener, homeIntent);
         startActivity(homeIntent,
-                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle());
+                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
+                        .toBundle());
         mFinishOnPause = true;
         return false;
     }
@@ -208,7 +207,7 @@
         }
         mWidgetCell.setPreview(PinItemDragListener.getPreview(mRequest));
 
-        mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
+        mAppWidgetManager = new WidgetManagerHelper(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this);
 
         PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(widgetInfo);
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 1b08723..707fd06 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -26,7 +26,6 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.SystemClock;
 import android.util.Log;
 import android.view.DragEvent;
 import android.view.View;
@@ -36,8 +35,7 @@
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.states.InternalStateHandler;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
 import com.android.launcher3.widget.PendingItemDragHelper;
 
 import java.util.UUID;
@@ -45,8 +43,8 @@
 /**
  * {@link DragSource} for handling drop from a different window.
  */
-public abstract class BaseItemDragListener extends InternalStateHandler implements
-        View.OnDragListener, DragSource, DragOptions.PreDragCondition {
+public abstract class BaseItemDragListener implements View.OnDragListener, DragSource,
+        DragOptions.PreDragCondition, SchedulerCallback<Launcher> {
 
     private static final String TAG = "BaseItemDragListener";
 
@@ -64,7 +62,6 @@
 
     protected Launcher mLauncher;
     private DragController mDragController;
-    private long mDragStartTime;
 
     public BaseItemDragListener(Rect previewRect, int previewBitmapWidth, int previewViewWidth) {
         mPreviewRect = previewRect;
@@ -95,7 +92,7 @@
             postCleanup();
             return false;
         }
-        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED || !mDragController.isDragging()) {
             if (onDragStart(event)) {
                 return true;
             } else {
@@ -103,7 +100,7 @@
                 return false;
             }
         }
-        return mDragController.onDragEvent(mDragStartTime, event);
+        return mDragController.onDragEvent(event);
     }
 
     protected boolean onDragStart(DragEvent event) {
@@ -119,7 +116,7 @@
 
         Point downPos = new Point((int) event.getX(), (int) event.getY());
         DragOptions options = new DragOptions();
-        options.systemDndStartPoint = downPos;
+        options.simulatedDndStartPoint = downPos;
         options.preDragCondition = preDragCondition;
 
         // We use drag event position as the screenPos for the preview image. Since mPreviewRect
@@ -129,7 +126,6 @@
         // to source window.
         createDragHelper().startDrag(new Rect(mPreviewRect),
                 mPreviewBitmapWidth, mPreviewViewWidth, downPos, this, options);
-        mDragStartTime = SystemClock.uptimeMillis();
         return true;
     }
 
@@ -165,7 +161,6 @@
     }
 
     protected void postCleanup() {
-        clearReference();
         if (mLauncher != null) {
             // Remove any drag params from the launcher intent since the drag operation is complete.
             Intent newIntent = new Intent(mLauncher.getIntent());
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index dcdf5d6..03028d3 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.Utilities.ATLEAST_Q;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.animation.ValueAnimator;
 import android.content.ComponentName;
@@ -27,8 +28,6 @@
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.os.IBinder;
-import android.util.Log;
 import android.view.DragEvent;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -38,16 +37,13 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
-import com.android.launcher3.testing.TestProtocol;
+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.Thunk;
 import com.android.launcher3.util.TouchController;
-import com.android.launcher3.util.UiThreadHelper;
 
 import java.util.ArrayList;
 
@@ -63,11 +59,11 @@
      */
     private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
 
-    @Thunk Launcher mLauncher;
-    private FlingToDeleteHelper mFlingToDeleteHelper;
+    private final Launcher mLauncher;
+    private final FlingToDeleteHelper mFlingToDeleteHelper;
 
     // temporaries to avoid gc thrash
-    private Rect mRectTemp = new Rect();
+    private final Rect mRectTemp = new Rect();
     private final int[] mCoordinatesTemp = new int[2];
 
     /**
@@ -79,33 +75,24 @@
     /** Options controlling the drag behavior. */
     private DragOptions mOptions;
 
-    /** X coordinate of the down event. */
-    private int mMotionDownX;
+    /** Coordinate for motion down event */
+    private final Point mMotionDown = new Point();
+    /** Coordinate for last touch event **/
+    private final Point mLastTouch = new Point();
 
-    /** Y coordinate of the down event. */
-    private int mMotionDownY;
+    private final Point mTmpPoint = new Point();
 
     private DropTarget.DragObject mDragObject;
 
     /** Who can receive drop events */
-    private ArrayList<DropTarget> mDropTargets = new ArrayList<>();
-    private ArrayList<DragListener> mListeners = new ArrayList<>();
-
-    /** The window token used as the parent for the DragView. */
-    private IBinder mWindowToken;
-
-    private View mMoveTarget;
+    private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
+    private final ArrayList<DragListener> mListeners = new ArrayList<>();
 
     private DropTarget mLastDropTarget;
 
-    private final int[] mLastTouch = new int[2];
-    private long mLastTouchUpTime = -1;
     private int mLastTouchClassification;
     private int mDistanceSinceScroll = 0;
 
-    private int mTmpPoint[] = new int[2];
-    private Rect mDragLayerRect = new Rect();
-
     private boolean mIsInPreDrag;
 
     /**
@@ -142,6 +129,8 @@
      *
      * @param b The bitmap to display as the drag image.  It will be re-scaled to the
      *          enlarged size.
+     * @param originalView The source view (ie. icon, widget etc.) that is being dragged
+     *          and which the DragView represents
      * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
      * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
      * @param source An object representing where the drag originated
@@ -149,32 +138,32 @@
      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
      *          Makes dragging feel more precise, e.g. you can clip out a transparent border
      */
-    public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
+    public DragView startDrag(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY,
             DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
             float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
         if (PROFILE_DRAWING_DURING_DRAG) {
             android.os.Debug.startMethodTracing("Launcher");
         }
 
-        // Hide soft keyboard, if visible
-        UiThreadHelper.hideKeyboardAsync(mLauncher, mWindowToken);
+        mLauncher.hideKeyboard();
         AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_DISCOVERY_BOUNCE);
 
         mOptions = options;
-        if (mOptions.systemDndStartPoint != null) {
-            mMotionDownX = mOptions.systemDndStartPoint.x;
-            mMotionDownY = mOptions.systemDndStartPoint.y;
+        if (mOptions.simulatedDndStartPoint != null) {
+            mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
+            mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
         }
 
-        final int registrationX = mMotionDownX - dragLayerX;
-        final int registrationY = mMotionDownY - dragLayerY;
+        final int registrationX = mMotionDown.x - dragLayerX;
+        final int registrationY = mMotionDown.y - dragLayerY;
 
         final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
 
         mLastDropTarget = null;
 
-        mDragObject = new DropTarget.DragObject();
+        mDragObject = new DropTarget.DragObject(mLauncher.getApplicationContext());
+        mDragObject.originalView = originalView;
 
         mIsInPreDrag = mOptions.preDragCondition != null
                 && !mOptions.preDragCondition.shouldStartDrag(0);
@@ -186,23 +175,18 @@
                 registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
         dragView.setItemInfo(dragInfo);
         mDragObject.dragComplete = false;
-        if (mOptions.isAccessibleDrag) {
-            // For an accessible drag, we assume the view is being dragged from the center.
-            mDragObject.xOffset = b.getWidth() / 2;
-            mDragObject.yOffset = b.getHeight() / 2;
-            mDragObject.accessibleDrag = true;
-        } else {
-            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
-            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
-            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
 
-            mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions);
+        mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
+        mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
+
+        mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
+        if (!mOptions.isAccessibleDrag) {
+            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
         }
 
         mDragObject.dragSource = source;
         mDragObject.dragInfo = dragInfo;
-        mDragObject.originalDragInfo = new ItemInfo();
-        mDragObject.originalDragInfo.copyFrom(dragInfo);
+        mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
 
         if (dragOffset != null) {
             dragView.setDragVisualizeOffset(new Point(dragOffset));
@@ -212,7 +196,7 @@
         }
 
         mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-        dragView.show(mLastTouch[0], mLastTouch[1]);
+        dragView.show(mLastTouch.x, mLastTouch.y);
         mDistanceSinceScroll = 0;
 
         if (!mIsInPreDrag) {
@@ -221,8 +205,13 @@
             mOptions.preDragCondition.onPreDragStart(mDragObject);
         }
 
-        handleMoveEvent(mLastTouch[0], mLastTouch[1]);
+        handleMoveEvent(mLastTouch.x, mLastTouch.y);
         mLauncher.getUserEventDispatcher().resetActionDurationMillis();
+
+        if (!mLauncher.isTouchInProgress() && options.simulatedDndStartPoint == null) {
+            // If it is an internal drag and the touch is already complete, cancel immediately
+            MAIN_EXECUTOR.submit(this::cancelDrag);
+        }
         return dragView;
     }
 
@@ -338,7 +327,7 @@
                 }
             }
         };
-        mDragObject.dragView.animateTo(mMotionDownX, mMotionDownY, onCompleteRunnable, duration);
+        mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration);
     }
 
     private void callOnDragEnd() {
@@ -367,30 +356,17 @@
     /**
      * Clamps the position to the drag layer bounds.
      */
-    private int[] getClampedDragLayerPos(float x, float y) {
-        mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
-        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
-        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
+    private Point getClampedDragLayerPos(float x, float y) {
+        mLauncher.getDragLayer().getLocalVisibleRect(mRectTemp);
+        mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1));
+        mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1));
         return mTmpPoint;
     }
 
-    public long getLastGestureUpTime() {
-        if (mDragDriver != null) {
-            return System.currentTimeMillis();
-        } else {
-            return mLastTouchUpTime;
-        }
-    }
-
-    public void resetLastGestureUpTime() {
-        mLastTouchUpTime = -1;
-    }
-
     @Override
     public void onDriverDragMove(float x, float y) {
-        final int[] dragLayerPos = getClampedDragLayerPos(x, y);
-
-        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
+        Point dragLayerPos = getClampedDragLayerPos(x, y);
+        handleMoveEvent(dragLayerPos.x, dragLayerPos.y);
     }
 
     @Override
@@ -424,66 +400,40 @@
     /**
      * Call this from a drag source view.
      */
+    @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (mOptions != null && mOptions.isAccessibleDrag) {
             return false;
         }
 
-        // Update the velocity tracker
-        mFlingToDeleteHelper.recordMotionEvent(ev);
+        Point dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
+        mLastTouch.set(dragLayerPos.x,  dragLayerPos.y);
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            // Remember location of down touch
+            mMotionDown.set(dragLayerPos.x,  dragLayerPos.y);
+        }
 
-        final int action = ev.getAction();
-        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
-        final int dragLayerX = dragLayerPos[0];
-        final int dragLayerY = dragLayerPos[1];
-        mLastTouch[0] = dragLayerX;
-        mLastTouch[1] = dragLayerY;
         if (ATLEAST_Q) {
             mLastTouchClassification = ev.getClassification();
         }
-
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                // Remember location of down touch
-                mMotionDownX = dragLayerX;
-                mMotionDownY = dragLayerY;
-                break;
-            case MotionEvent.ACTION_UP:
-                mLastTouchUpTime = System.currentTimeMillis();
-                break;
-        }
-
         return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
     }
 
     /**
      * Call this from a drag source view.
      */
-    public boolean onDragEvent(long dragStartTime, DragEvent event) {
-        mFlingToDeleteHelper.recordDragEvent(dragStartTime, event);
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mDragDriver != null && mDragDriver.onTouchEvent(ev);
+    }
+
+    /**
+     * Call this from a drag source view.
+     */
+    public boolean onDragEvent(DragEvent event) {
         return mDragDriver != null && mDragDriver.onDragEvent(event);
     }
 
-    /**
-     * Call this from a drag view.
-     */
-    public void onDragViewAnimationEnd() {
-        if (mDragDriver != null) {
-            mDragDriver.onDragViewAnimationEnd();
-        }
-    }
-
-    /**
-     * Sets the view that should handle move events.
-     */
-    public void setMoveTarget(View view) {
-        mMoveTarget = view;
-    }
-
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
-    }
-
     private void handleMoveEvent(int x, int y) {
         mDragObject.dragView.move(x, y);
 
@@ -495,9 +445,8 @@
         checkTouchMove(dropTarget);
 
         // Check if we are hovering over the scroll areas
-        mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
-        mLastTouch[0] = x;
-        mLastTouch[1] = y;
+        mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y);
+        mLastTouch.set(x, y);
 
         int distanceDragged = mDistanceSinceScroll;
         if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
@@ -515,7 +464,7 @@
 
     public void forceTouchMove() {
         int[] dummyCoordinates = mCoordinatesTemp;
-        DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates);
+        DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, dummyCoordinates);
         mDragObject.x = dummyCoordinates[0];
         mDragObject.y = dummyCoordinates[1];
         checkTouchMove(dropTarget);
@@ -539,42 +488,6 @@
     }
 
     /**
-     * Call this from a drag source view.
-     */
-    public boolean onControllerTouchEvent(MotionEvent ev) {
-        if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) {
-            return false;
-        }
-
-        // Update the velocity tracker
-        mFlingToDeleteHelper.recordMotionEvent(ev);
-
-        final int action = ev.getAction();
-        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
-        final int dragLayerX = dragLayerPos[0];
-        final int dragLayerY = dragLayerPos[1];
-
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                // Remember where the motion event started
-                mMotionDownX = dragLayerX;
-                mMotionDownY = dragLayerY;
-                break;
-        }
-
-        return mDragDriver.onTouchEvent(ev);
-    }
-
-    /**
-     * Since accessible drag and drop won't cause the same sequence of touch events, we manually
-     * inject the appropriate state.
-     */
-    public void prepareAccessibleDrag(int x, int y) {
-        mMotionDownX = x;
-        mMotionDownY = y;
-    }
-
-    /**
      * As above, since accessible drag and drop won't cause the same sequence of touch events,
      * we manually ensure appropriate drag and drop events get emulated for accessible drag.
      */
@@ -594,9 +507,6 @@
     }
 
     private void drop(DropTarget dropTarget, Runnable flingAnimation) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragController.drop");
-        }
         final int[] coordinates = mCoordinatesTemp;
         mDragObject.x = coordinates[0];
         mDragObject.y = coordinates[1];
@@ -667,10 +577,6 @@
         return mLauncher.getWorkspace();
     }
 
-    public void setWindowToken(IBinder token) {
-        mWindowToken = token;
-    }
-
     /**
      * Sets the drag listener which will be notified when a drag starts or ends.
      */
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 01e0f92..d4ce308 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -16,19 +16,19 @@
 
 package com.android.launcher3.dragndrop;
 
-import android.content.Context;
-import android.util.Log;
+import android.os.SystemClock;
 import android.view.DragEvent;
 import android.view.MotionEvent;
 
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.testing.TestProtocol;
+import java.util.function.Consumer;
 
 /**
  * Base class for driving a drag/drop operation.
  */
 public abstract class DragDriver {
+
     protected final EventListener mEventListener;
+    protected final Consumer<MotionEvent> mSecondaryEventConsumer;
 
     public interface EventListener {
         void onDriverDragMove(float x, float y);
@@ -37,137 +37,175 @@
         void onDriverDragCancel();
     }
 
-    public DragDriver(EventListener eventListener) {
+    public DragDriver(EventListener eventListener, Consumer<MotionEvent> sec) {
         mEventListener = eventListener;
+        mSecondaryEventConsumer = sec;
     }
 
     /**
-     * Handles ending of the DragView animation.
+     * Called to handle system touch event
      */
-    public void onDragViewAnimationEnd() { }
-
     public boolean onTouchEvent(MotionEvent ev) {
-        final int action = ev.getAction();
-
-        switch (action) {
-            case MotionEvent.ACTION_MOVE:
-                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_UP:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_UP");
-                }
-                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
-                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_CANCEL");
-                }
-                mEventListener.onDriverDragCancel();
-                break;
-        }
-
-        return true;
+        return false;
     }
 
-    public abstract boolean onDragEvent (DragEvent event);
-
-
+    /**
+     * Called to handle system touch intercept event
+     */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        final int action = ev.getAction();
-
-        switch (action) {
-            case MotionEvent.ACTION_UP:
-                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                mEventListener.onDriverDragCancel();
-                break;
-        }
-
-        return true;
+        return false;
     }
 
-    public static DragDriver create(Context context, DragController dragController,
-            DragObject dragObject, DragOptions options) {
-        if (options.systemDndStartPoint != null) {
-            return new SystemDragDriver(dragController, context, dragObject);
+    /**
+     * Called to handle system drag event
+     */
+    public boolean onDragEvent(DragEvent event) {
+        return false;
+    }
+
+    /**
+     * Created a driver for handing the actual events
+     */
+    public static DragDriver create(DragController dragController, DragOptions options,
+            Consumer<MotionEvent> sec) {
+        if (options.simulatedDndStartPoint != null) {
+            if  (options.isAccessibleDrag) {
+                return null;
+            }
+            return new SystemDragDriver(dragController, sec);
         } else {
-            return new InternalDragDriver(dragController);
+            return new InternalDragDriver(dragController, sec);
+        }
+    }
+
+    /**
+     * Class for driving a system (i.e. framework) drag/drop operation.
+     */
+    static class SystemDragDriver extends DragDriver {
+
+        private final long mDragStartTime;
+        float mLastX = 0;
+        float mLastY = 0;
+
+        SystemDragDriver(DragController dragController, Consumer<MotionEvent> sec) {
+            super(dragController, sec);
+            mDragStartTime = SystemClock.uptimeMillis();
+        }
+
+        @Override
+        public boolean onInterceptTouchEvent(MotionEvent ev) {
+            return false;
+        }
+
+        /**
+         * It creates a temporary {@link MotionEvent} object for secondary consumer
+         */
+        private void simulateSecondaryMotionEvent(DragEvent event) {
+            final int motionAction;
+            switch (event.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    motionAction = MotionEvent.ACTION_DOWN;
+                    break;
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    motionAction = MotionEvent.ACTION_MOVE;
+                    break;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    motionAction = MotionEvent.ACTION_UP;
+                    break;
+                default:
+                    return;
+            }
+            MotionEvent emulatedEvent = MotionEvent.obtain(mDragStartTime,
+                    SystemClock.uptimeMillis(), motionAction, event.getX(), event.getY(), 0);
+            mSecondaryEventConsumer.accept(emulatedEvent);
+            emulatedEvent.recycle();
+        }
+
+        @Override
+        public boolean onDragEvent(DragEvent event) {
+            simulateSecondaryMotionEvent(event);
+            final int action = event.getAction();
+
+            switch (action) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENTERED:
+                    return true;
+
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    mEventListener.onDriverDragMove(event.getX(), event.getY());
+                    return true;
+
+                case DragEvent.ACTION_DROP:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    mEventListener.onDriverDragMove(event.getX(), event.getY());
+                    mEventListener.onDriverDragEnd(mLastX, mLastY);
+                    return true;
+                case DragEvent.ACTION_DRAG_EXITED:
+                    mEventListener.onDriverDragExitWindow();
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENDED:
+                    mEventListener.onDriverDragCancel();
+                    return true;
+
+                default:
+                    return false;
+            }
+        }
+    }
+
+    /**
+     * Class for driving an internal (i.e. not using framework) drag/drop operation.
+     */
+    static class InternalDragDriver extends DragDriver {
+        InternalDragDriver(DragController dragController, Consumer<MotionEvent> sec) {
+            super(dragController, sec);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            mSecondaryEventConsumer.accept(ev);
+            final int action = ev.getAction();
+
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    mEventListener.onDriverDragMove(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_UP:
+                    mEventListener.onDriverDragMove(ev.getX(), ev.getY());
+                    mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    mEventListener.onDriverDragCancel();
+                    break;
+            }
+
+            return true;
+        }
+
+
+        public boolean onInterceptTouchEvent(MotionEvent ev) {
+            mSecondaryEventConsumer.accept(ev);
+            final int action = ev.getAction();
+
+            switch (action) {
+                case MotionEvent.ACTION_UP:
+                    mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    mEventListener.onDriverDragCancel();
+                    break;
+            }
+            return true;
         }
     }
 }
 
-/**
- * Class for driving a system (i.e. framework) drag/drop operation.
- */
-class SystemDragDriver extends DragDriver {
 
-    float mLastX = 0;
-    float mLastY = 0;
-
-    SystemDragDriver(DragController dragController, Context context, DragObject dragObject) {
-        super(dragController);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        return false;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        return false;
-    }
-
-    @Override
-    public boolean onDragEvent (DragEvent event) {
-        final int action = event.getAction();
-
-        switch (action) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                return true;
-
-            case DragEvent.ACTION_DRAG_ENTERED:
-                return true;
-
-            case DragEvent.ACTION_DRAG_LOCATION:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                mEventListener.onDriverDragMove(event.getX(), event.getY());
-                return true;
-
-            case DragEvent.ACTION_DROP:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                mEventListener.onDriverDragMove(event.getX(), event.getY());
-                mEventListener.onDriverDragEnd(mLastX, mLastY);
-                return true;
-            case DragEvent.ACTION_DRAG_EXITED:
-                mEventListener.onDriverDragExitWindow();
-                return true;
-
-            case DragEvent.ACTION_DRAG_ENDED:
-                mEventListener.onDriverDragCancel();
-                return true;
-
-            default:
-                return false;
-        }
-    }
-}
-
-/**
- * Class for driving an internal (i.e. not using framework) drag/drop operation.
- */
-class InternalDragDriver extends DragDriver {
-    InternalDragDriver(DragController dragController) {
-        super(dragController);
-    }
-
-    @Override
-    public boolean onDragEvent (DragEvent event) { return false; }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 0b6d8fb..ddf44ca 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -17,10 +17,7 @@
 
 package com.android.launcher3.dragndrop;
 
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.getMode;
-import static android.view.View.MeasureSpec.getSize;
-
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 
 import android.animation.Animator;
@@ -33,15 +30,12 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.TextView;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.CellLayout;
@@ -50,17 +44,12 @@
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.views.Transposable;
 
 import java.util.ArrayList;
 
@@ -77,11 +66,11 @@
     public static final int ANIMATION_END_DISAPPEAR = 0;
     public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
 
-    @Thunk DragController mDragController;
+    private DragController mDragController;
 
     // Variables relating to animation of views after drop
     private ValueAnimator mDropAnim = null;
-    private final TimeInterpolator mCubicEaseOutInterpolator = Interpolators.DEACCEL_1_5;
+
     @Thunk DragView mDropView = null;
     @Thunk int mAnchorViewInitialScrollX = 0;
     @Thunk View mAnchorView = null;
@@ -96,6 +85,9 @@
     private final WorkspaceAndHotseatScrim mWorkspaceScrim;
     private final OverviewScrim mOverviewScrim;
 
+    // View that should handle move events
+    private View mMoveTarget;
+
     /**
      * Used to create a new DragLayer from XML.
      *
@@ -117,11 +109,13 @@
     public void setup(DragController dragController, Workspace workspace) {
         mDragController = dragController;
         mWorkspaceScrim.setWorkspace(workspace);
+        mMoveTarget = workspace;
         recreateControllers();
     }
 
+    @Override
     public void recreateControllers() {
-        mControllers = UiFactory.createTouchControllers(mActivity);
+        mControllers = mActivity.createTouchControllers();
     }
 
     public ViewGroupFocusHelper getFocusIndicatorHelper() {
@@ -223,7 +217,7 @@
     @Override
     public boolean dispatchUnhandledMove(View focused, int direction) {
         return super.dispatchUnhandledMove(focused, direction)
-                || mDragController.dispatchUnhandledMove(focused, direction);
+                || mMoveTarget.dispatchUnhandledMove(focused, direction);
     }
 
     @Override
@@ -254,59 +248,57 @@
 
     public void animateViewIntoPosition(DragView dragView, final View child, int duration,
             View anchorView) {
+
         ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
         CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
         parentChildren.measureChild(child);
+        parentChildren.layoutChild(child);
 
-        Rect r = new Rect();
-        getViewRectRelativeToSelf(dragView, r);
+        Rect dragViewBounds = new Rect();
+        getViewRectRelativeToSelf(dragView, dragViewBounds);
+        final int fromX = dragViewBounds.left;
+        final int fromY = dragViewBounds.top;
 
         float coord[] = new float[2];
         float childScale = child.getScaleX();
+
         coord[0] = lp.x + (child.getMeasuredWidth() * (1 - childScale) / 2);
         coord[1] = lp.y + (child.getMeasuredHeight() * (1 - childScale) / 2);
 
         // Since the child hasn't necessarily been laid out, we force the lp to be updated with
         // the correct coordinates (above) and use these to determine the final location
         float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
+
         // We need to account for the scale of the child itself, as the above only accounts for
         // for the scale in parents.
         scale *= childScale;
         int toX = Math.round(coord[0]);
         int toY = Math.round(coord[1]);
+
         float toScale = scale;
-        if (child instanceof TextView) {
-            TextView tv = (TextView) child;
-            // Account for the source scale of the icon (ie. from AllApps to Workspace, in which
-            // the workspace may have smaller icon bounds).
-            toScale = scale / dragView.getIntrinsicIconScaleFactor();
 
-            // The child may be scaled (always about the center of the view) so to account for it,
-            // we have to offset the position by the scaled size.  Once we do that, we can center
-            // the drag view about the scaled child view.
-            // padding will remain constant (does not scale with size)
-            toY += tv.getPaddingTop();
-            toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
-            if (dragView.getDragVisualizeOffset() != null) {
-                toY -=  Math.round(toScale * dragView.getDragVisualizeOffset().y);
-            }
+        if (child instanceof DraggableView) {
+            // This code is fairly subtle. Please verify drag and drop is pixel-perfect in a number
+            // of scenarios before modifying (from all apps, from workspace, different grid-sizes,
+            // shortcuts from in and out of Launcher etc).
+            DraggableView d = (DraggableView) child;
+            Rect destRect = new Rect();
+            d.getWorkspaceVisualDragBounds(destRect);
 
-            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
-        } else if (child instanceof FolderIcon) {
-            // Account for holographic blur padding on the drag view
-            toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop()));
-            toY -= scale * dragView.getBlurSizeOutline() / 2;
-            toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
-            // Center in the x coordinate about the target's drawable
-            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
-        } else {
-            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
-            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
-                    - child.getMeasuredWidth()))) / 2;
+            // In most cases this additional scale factor should be a no-op (1). It mainly accounts
+            // for alternate grids where the source and destination icon sizes are different
+            toScale *= ((1f * destRect.width())
+                    / (dragView.getMeasuredWidth() - dragView.getBlurSizeOutline()));
+
+            // This accounts for the offset of the DragView created by scaling it about its
+            // center as it animates into place.
+            float scaleShiftX = dragView.getMeasuredWidth() * (1 - toScale) / 2;
+            float scaleShiftY = dragView.getMeasuredHeight() * (1 - toScale) / 2;
+
+            toX += scale * destRect.left - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftX;
+            toY += scale * destRect.top - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftY;
         }
 
-        final int fromX = r.left;
-        final int fromY = r.top;
         child.setVisibility(INVISIBLE);
         Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE);
         animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
@@ -361,7 +353,7 @@
         if (duration < 0) {
             duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
             if (dist < maxDist) {
-                duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
+                duration *= DEACCEL_1_5.getInterpolation(dist / maxDist);
             }
             duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
         }
@@ -369,7 +361,7 @@
         // Fall back to cubic ease out interpolator for the animation if none is specified
         TimeInterpolator interpolator = null;
         if (alphaInterpolator == null || motionInterpolator == null) {
-            interpolator = mCubicEaseOutInterpolator;
+            interpolator = DEACCEL_1_5;
         }
 
         // Animate the view
@@ -477,14 +469,14 @@
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         updateChildIndices();
-        UiFactory.onLauncherStateOrFocusChanged(mActivity);
+        mActivity.onDragLayerHierarchyChanged();
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         updateChildIndices();
-        UiFactory.onLauncherStateOrFocusChanged(mActivity);
+        mActivity.onDragLayerHierarchyChanged();
     }
 
     @Override
@@ -537,6 +529,9 @@
         mOverviewScrim.updateCurrentScrimmedView(this);
         mFocusIndicatorHelper.draw(canvas);
         super.dispatchDraw(canvas);
+        if (mOverviewScrim.getScrimmedView() == null) {
+            mOverviewScrim.draw(canvas);
+        }
     }
 
     @Override
@@ -567,145 +562,4 @@
     public OverviewScrim getOverviewScrim() {
         return mOverviewScrim;
     }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        RotationMode rotation = mActivity.getRotationMode();
-        int count = getChildCount();
-
-        if (!rotation.isTransposed
-                || getMode(widthMeasureSpec) != EXACTLY
-                || getMode(heightMeasureSpec) != EXACTLY) {
-
-            for (int i = 0; i < count; i++) {
-                final View child = getChildAt(i);
-                child.setRotation(rotation.surfaceRotation);
-            }
-            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        } else {
-
-            for (int i = 0; i < count; i++) {
-                final View child = getChildAt(i);
-                if (child.getVisibility() == GONE) {
-                    continue;
-                }
-                if (!(child instanceof Transposable)) {
-                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
-                } else {
-                    measureChildWithMargins(child, heightMeasureSpec, 0, widthMeasureSpec, 0);
-
-                    child.setPivotX(child.getMeasuredWidth() / 2);
-                    child.setPivotY(child.getMeasuredHeight() / 2);
-                    child.setRotation(rotation.surfaceRotation);
-                }
-            }
-            setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        RotationMode rotation = mActivity.getRotationMode();
-        if (!rotation.isTransposed) {
-            super.onLayout(changed, left, top, right, bottom);
-            return;
-        }
-
-        final int count = getChildCount();
-
-        final int parentWidth = right - left;
-        final int parentHeight = bottom - top;
-
-        for (int i = 0; i < count; i++) {
-            final View child = getChildAt(i);
-            if (child.getVisibility() == GONE) {
-                continue;
-            }
-
-            final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
-
-            if (lp instanceof LayoutParams) {
-                final LayoutParams dlp = (LayoutParams) lp;
-                if (dlp.customPosition) {
-                    child.layout(dlp.x, dlp.y, dlp.x + dlp.width, dlp.y + dlp.height);
-                    continue;
-                }
-            }
-
-            final int width = child.getMeasuredWidth();
-            final int height = child.getMeasuredHeight();
-
-            int childLeft;
-            int childTop;
-
-            int gravity = lp.gravity;
-            if (gravity == -1) {
-                gravity = Gravity.TOP | Gravity.START;
-            }
-
-            final int layoutDirection = getLayoutDirection();
-
-            int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
-
-            if (child instanceof Transposable) {
-                absoluteGravity = rotation.toNaturalGravity(absoluteGravity);
-
-                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
-                    case Gravity.CENTER_HORIZONTAL:
-                        childTop = (parentHeight - height) / 2 +
-                                lp.topMargin - lp.bottomMargin;
-                        break;
-                    case Gravity.RIGHT:
-                        childTop = width / 2 + lp.rightMargin - height / 2;
-                        break;
-                    case Gravity.LEFT:
-                    default:
-                        childTop = parentHeight - lp.leftMargin - width / 2 - height / 2;
-                }
-
-                switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
-                    case Gravity.CENTER_VERTICAL:
-                        childLeft = (parentWidth - width) / 2 +
-                                lp.leftMargin - lp.rightMargin;
-                        break;
-                    case Gravity.BOTTOM:
-                        childLeft = parentWidth - width / 2 - height / 2 - lp.bottomMargin;
-                        break;
-                    case Gravity.TOP:
-                    default:
-                        childLeft = height / 2 - width / 2 + lp.topMargin;
-                }
-            } else {
-                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
-                    case Gravity.CENTER_HORIZONTAL:
-                        childLeft = (parentWidth - width) / 2 +
-                                lp.leftMargin - lp.rightMargin;
-                        break;
-                    case Gravity.RIGHT:
-                        childLeft = parentWidth - width - lp.rightMargin;
-                        break;
-                    case Gravity.LEFT:
-                    default:
-                        childLeft = lp.leftMargin;
-                }
-
-                switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
-                    case Gravity.TOP:
-                        childTop = lp.topMargin;
-                        break;
-                    case Gravity.CENTER_VERTICAL:
-                        childTop = (parentHeight - height) / 2 +
-                                lp.topMargin - lp.bottomMargin;
-                        break;
-                    case Gravity.BOTTOM:
-                        childTop = parentHeight - height - lp.bottomMargin;
-                        break;
-                    default:
-                        childTop = lp.topMargin;
-                }
-            }
-
-            child.layout(childLeft, childTop, childLeft + width, childTop + height);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index 2d19f36..959602b 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -28,8 +28,11 @@
     /** Whether or not an accessible drag operation is in progress. */
     public boolean isAccessibleDrag = false;
 
-    /** Specifies the start location for the system DnD, null when using internal DnD */
-    public Point systemDndStartPoint = null;
+    /**
+     * Specifies the start location for a simulated DnD (like system drag or accessibility drag),
+     * null when using internal DnD
+     */
+    public Point simulatedDndStartPoint = null;
 
     /** Determines when a pre-drag should transition to a drag. By default, this is immediate. */
     public PreDragCondition preDragCondition = null;
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index f66d07e..de0fa1a 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -19,8 +19,6 @@
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.FloatArrayEvaluator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -48,21 +46,21 @@
 
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.FirstFrameAnimatorHelper;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
 
 import java.util.Arrays;
 
-public class DragView extends View implements LauncherStateManager.StateListener {
+public class DragView extends View implements StateListener<LauncherState> {
     private static final ColorMatrix sTempMatrix1 = new ColorMatrix();
     private static final ColorMatrix sTempMatrix2 = new ColorMatrix();
 
@@ -146,15 +144,6 @@
             }
         });
 
-        mAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (!mAnimationCancelled) {
-                    mDragController.onDragViewAnimationEnd();
-                }
-            }
-        });
-
         mBitmap = bitmap;
         setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
 
@@ -187,9 +176,6 @@
     }
 
     @Override
-    public void onStateTransitionStart(LauncherState toState) { }
-
-    @Override
     public void onStateTransitionComplete(LauncherState finalState) {
         setVisibility((finalState == LauncherState.NORMAL
                 || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE);
@@ -216,8 +202,7 @@
                 Object[] outObj = new Object[1];
                 int w = mBitmap.getWidth();
                 int h = mBitmap.getHeight();
-                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h,
-                        false /* flattenDrawable */, outObj);
+                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
 
                 if (dr instanceof AdaptiveIconDrawable) {
                     int blurMargin = (int) mLauncher.getResources()
diff --git a/src/com/android/launcher3/dragndrop/DraggableView.java b/src/com/android/launcher3/dragndrop/DraggableView.java
new file mode 100644
index 0000000..f7dcf6b
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/DraggableView.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dragndrop;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.util.SafeCloseable;
+
+/**
+ * Interface defining methods required for drawing and previewing DragViews, drag previews, and
+ * related animations
+ */
+public interface DraggableView {
+    int DRAGGABLE_ICON = 0;
+    int DRAGGABLE_WIDGET = 1;
+
+    /**
+     * Static ctr for a simple instance which just returns the view type.
+     */
+    static DraggableView ofType(int type) {
+        return () -> type;
+    }
+
+    /**
+     * Certain handling of DragViews depend only on whether this is an Icon Type item or a Widget
+     * Type item.
+     *
+     * @return DRAGGABLE_ICON or DRAGGABLE_WIDGET as appropriate
+     */
+    int getViewType();
+
+    /**
+     * Before rendering as a DragView bitmap, some views need a preparation step. Returns a
+     * callback to clear any preparation work
+     */
+    @NonNull default SafeCloseable prepareDrawDragView() {
+        return () -> { };
+    }
+
+    /**
+     * If an actual View subclass, this method returns the rectangle (within the View's coordinates)
+     * of the visual region that should get dragged. This is used to extract exactly that element
+     * as well as to offset that element as appropriate for various animations
+     *
+     * @param bounds Visual bounds in the views coordinates will be written here.
+     */
+    default void getWorkspaceVisualDragBounds(Rect bounds) { }
+
+    /**
+     * Same as above, but accounts for differing icon sizes between source and destination
+     *
+     * @param bounds Visual bounds in the views coordinates will be written here.
+     */
+    default void getSourceVisualDragBounds(Rect bounds) {
+        getWorkspaceVisualDragBounds(bounds);
+    }
+}
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 06b5c40..7788f93 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.dragndrop;
 
 import android.graphics.PointF;
-import android.os.SystemClock;
-import android.view.DragEvent;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -55,31 +53,6 @@
         mVelocityTracker.addMovement(ev);
     }
 
-    /**
-     * Same as {@link #recordMotionEvent}. It creates a temporary {@link MotionEvent} object
-     * using {@param event} for tracking velocity.
-     */
-    public void recordDragEvent(long dragStartTime, DragEvent event) {
-        final int motionAction;
-        switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                motionAction = MotionEvent.ACTION_DOWN;
-                break;
-            case DragEvent.ACTION_DRAG_LOCATION:
-                motionAction = MotionEvent.ACTION_MOVE;
-                break;
-            case DragEvent.ACTION_DRAG_ENDED:
-                motionAction = MotionEvent.ACTION_UP;
-                break;
-            default:
-                return;
-        }
-        MotionEvent emulatedEvent = MotionEvent.obtain(dragStartTime, SystemClock.uptimeMillis(),
-                motionAction, event.getX(), event.getY(), 0);
-        recordMotionEvent(emulatedEvent);
-        emulatedEvent.recycle();
-    }
-
     public void releaseVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index 0bb3fba..ea1fbdb 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -20,10 +20,10 @@
 
 import android.annotation.TargetApi;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -33,7 +33,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.ShiftedBitmapDrawable;
@@ -50,6 +49,7 @@
     private final Drawable mBadge;
     private final Path mMask;
     private final ConstantState mConstantState;
+    private static final Rect sTmpRect = new Rect();
 
     private FolderAdaptiveIcon(Drawable bg, Drawable fg, Drawable badge, Path mask) {
         super(bg, fg);
@@ -72,23 +72,14 @@
     public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon(
             Launcher launcher, int folderId, Point dragViewSize) {
         Preconditions.assertNonUiThread();
-        int margin = launcher.getResources()
-                .getDimensionPixelSize(R.dimen.blur_size_medium_outline);
-
-        // Allocate various bitmaps on the background thread, because why not!
-        int width = dragViewSize.x - margin;
-        int height = dragViewSize.y - margin;
-        if (width <= 0 || height <= 0) {
-            return null;
-        }
-        final Bitmap badge = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
 
         // Create the actual drawable on the UI thread to avoid race conditions with
         // FolderIcon draw pass
         try {
             return MAIN_EXECUTOR.submit(() -> {
                 FolderIcon icon = launcher.findFolderIcon(folderId);
-                return icon == null ? null : createDrawableOnUiThread(icon, badge, dragViewSize);
+                return icon == null ? null : createDrawableOnUiThread(icon, dragViewSize);
+
             }).get();
         } catch (Exception e) {
             Log.e(TAG, "Unable to create folder icon", e);
@@ -96,49 +87,50 @@
         }
     }
 
-    /**
-     * Initializes various bitmaps on the UI thread and returns the final drawable.
-     */
     private static FolderAdaptiveIcon createDrawableOnUiThread(FolderIcon icon,
-            Bitmap badgeBitmap, Point dragViewSize) {
+                                                               Point dragViewSize) {
         Preconditions.assertUIThread();
-        float margin = icon.getResources().getDimension(R.dimen.blur_size_medium_outline) / 2;
 
-        Canvas c = new Canvas();
+        icon.getPreviewBounds(sTmpRect);
+
         PreviewBackground bg = icon.getFolderBackground();
 
-        // Initialize badge
-        c.setBitmap(badgeBitmap);
-        bg.drawShadow(c);
-        bg.drawBackgroundStroke(c);
-        icon.drawDot(c);
+        // assume square
+        assert (dragViewSize.x == dragViewSize.y);
+        final int previewSize = sTmpRect.width();
 
-        // Initialize preview
-        final float sizeScaleFactor = 1 + 2 * AdaptiveIconDrawable.getExtraInsetFraction();
-        final int previewWidth = (int) (dragViewSize.x * sizeScaleFactor);
-        final int previewHeight = (int) (dragViewSize.y * sizeScaleFactor);
+        final int margin = (dragViewSize.x - previewSize) / 2;
+        final float previewShiftX = -sTmpRect.left + margin;
+        final float previewShiftY = -sTmpRect.top + margin;
 
-        final float shiftFactor = AdaptiveIconDrawable.getExtraInsetFraction() / sizeScaleFactor;
-        final float previewShiftX = shiftFactor * previewWidth;
-        final float previewShiftY = shiftFactor * previewHeight;
-
-        Bitmap previewBitmap = BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight,
+        // Initialize badge, which consists of the outline stroke, shadow and dot; these
+        // must be rendered above the foreground
+        Bitmap badgeBmp = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y,
                 (canvas) -> {
-                    int count = canvas.save();
+                    canvas.save();
                     canvas.translate(previewShiftX, previewShiftY);
-                    icon.getPreviewItemManager().draw(canvas);
-                    canvas.restoreToCount(count);
+                    bg.drawShadow(canvas);
+                    bg.drawBackgroundStroke(canvas);
+                    icon.drawDot(canvas);
+                    canvas.restore();
                 });
 
         // Initialize mask
         Path mask = new Path();
         Matrix m = new Matrix();
-        m.setTranslate(margin, margin);
+        m.setTranslate(previewShiftX, previewShiftY);
         bg.getClipPath().transform(m, mask);
 
-        ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBitmap, margin, margin);
-        ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap,
-                margin - previewShiftX, margin - previewShiftY);
+        Bitmap previewBitmap = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y,
+                (canvas) -> {
+                    canvas.save();
+                    canvas.translate(previewShiftX, previewShiftY);
+                    icon.getPreviewItemManager().draw(canvas);
+                    canvas.restore();
+                });
+
+        ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBmp, 0, 0);
+        ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap, 0, 0);
 
         return new FolderAdaptiveIcon(new ColorDrawable(bg.getBgColor()), foreground, badge, mask);
     }
diff --git a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
index e36f607..a9389bc 100644
--- a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
+++ b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
@@ -2,7 +2,6 @@
 
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
@@ -11,6 +10,7 @@
 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;
 
 /**
@@ -88,11 +88,9 @@
             bitmapHeight = viewHeight;
         }
 
-        Bitmap preview = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
-        Canvas c = new Canvas(preview);
-        c.scale(scale, scale);
-        v.draw(c);
-        c.setBitmap(null);
-        return preview;
+        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 07eb0d6..bf3aa7f 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+
 import android.annotation.TargetApi;
 import android.appwidget.AppWidgetManager;
 import android.content.pm.LauncherApps.PinItemRequest;
@@ -28,17 +30,18 @@
 import android.widget.RemoteViews;
 
 import com.android.launcher3.DragSource;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingItemDragHelper;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 
+import java.util.ArrayList;
+
 /**
  * {@link DragSource} for handling drop from a different window. This object is initialized
  * in the source window and is passed on to the Launcher activity as an Intent extra.
@@ -68,7 +71,7 @@
     public boolean init(Launcher launcher, boolean alreadyOnHome) {
         super.init(launcher, alreadyOnHome);
         if (!alreadyOnHome) {
-            UiFactory.useFadeOutAnimationForLauncherStart(launcher, mCancelSignal);
+            launcher.useFadeOutAnimationForLauncherStart(mCancelSignal);
         }
         return false;
     }
@@ -104,9 +107,9 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
-            LauncherLogProto.Target targetParent) {
-        targetParent.containerType = LauncherLogProto.ContainerType.PINITEM;
+    public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
+            ArrayList<LauncherLogProto.Target> parents) {
+        parents.add(newContainerTarget(LauncherLogProto.ContainerType.PINITEM));
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index 91a31aa..9982b39 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -29,14 +29,16 @@
 import android.os.Process;
 
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.compat.LauncherAppsCompatVO;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.PinRequestHelper;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 
 /**
  * Extension of ShortcutConfigActivityInfo to be used in the confirmation prompt for pin item
@@ -86,9 +88,9 @@
         // Total duration for the drop animation to complete.
         long duration = mContext.getResources().getInteger(R.integer.config_dropAnimMaxDuration) +
                 LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY +
-                LauncherAnimUtils.SPRING_LOADED_TRANSITION_MS;
+                LauncherState.SPRING_LOADED.getTransitionDuration(Launcher.getLauncher(mContext));
         // Delay the actual accept() call until the drop animation is complete.
-        return LauncherAppsCompatVO.createWorkspaceItemFromPinItemRequest(
+        return PinRequestHelper.createWorkspaceItemFromPinItemRequest(
                 mContext, mRequest, duration);
     }
 
diff --git a/src/com/android/launcher3/dragndrop/PinWidgetFlowHandler.java b/src/com/android/launcher3/dragndrop/PinWidgetFlowHandler.java
index 9f617e4..c501ab5 100644
--- a/src/com/android/launcher3/dragndrop/PinWidgetFlowHandler.java
+++ b/src/com/android/launcher3/dragndrop/PinWidgetFlowHandler.java
@@ -25,8 +25,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 
 /**
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 8df5e7d..d01e189 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -16,11 +16,16 @@
 
 package com.android.launcher3.folder;
 
+import static android.text.TextUtils.isEmpty;
+
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -49,39 +54,40 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Alarm;
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.FolderInfo.FolderListener;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.OnAlarmListener;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.Workspace.ItemOperator;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
+import com.android.launcher3.accessibility.FolderAccessibilityHelper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.FolderInfo.FolderListener;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ClipPathView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -90,6 +96,10 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Represents a set of icons chosen by the user or generated by the system.
@@ -98,6 +108,12 @@
         View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
         View.OnFocusChangeListener, DragListener, ExtendedEditText.OnBackKeyListener {
     private static final String TAG = "Launcher.Folder";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Used for separating folder title when logging together.
+     */
+    private static final CharSequence FOLDER_LABEL_DELIMITER = "~";
 
     /**
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
@@ -140,15 +156,18 @@
     @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
     private AnimatorSet mCurrentAnimator;
+    private boolean mIsAnimatingClosed = false;
 
     protected final Launcher mLauncher;
     protected DragController mDragController;
     public FolderInfo mInfo;
+    private CharSequence mFromTitle;
+    private FromState mFromLabelState;
 
     @Thunk FolderIcon mFolderIcon;
 
     @Thunk FolderPagedView mContent;
-    public ExtendedEditText mFolderName;
+    public FolderNameEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
     protected View mFooter;
@@ -187,6 +206,8 @@
     @Thunk int mScrollHintDir = SCROLL_NONE;
     @Thunk int mCurrentScrollDir = SCROLL_NONE;
 
+    private StatsLogManager mStatsLogManager;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -198,10 +219,12 @@
         setAlwaysDrawnWithCacheEnabled(false);
 
         mLauncher = Launcher.getLauncher(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
         // reliable behavior when clicking the text field (since it will always gain focus on click).
         setFocusableInTouchMode(true);
+
     }
 
     @Override
@@ -220,7 +243,7 @@
                 & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
                 & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
-        mFolderName.forceDisableSuggestions(true);
+        mFolderName.forceDisableSuggestions(!FeatureFlags.FOLDER_NAME_SUGGEST.get());
 
         mFooter = findViewById(R.id.folder_footer);
 
@@ -248,16 +271,15 @@
             mDragController.addDragListener(this);
             if (options.isAccessibleDrag) {
                 mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                        mContent, CellLayout.FOLDER_ACCESSIBILITY_DRAG) {
-
-                    @Override
-                    protected void enableAccessibleDrag(boolean enable) {
-                        super.enableAccessibleDrag(enable);
-                        mFooter.setImportantForAccessibility(enable
-                                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-                    }
-                });
+                        mContent, FolderAccessibilityHelper::new) {
+                            @Override
+                            protected void enableAccessibleDrag(boolean enable) {
+                                super.enableAccessibleDrag(enable);
+                                mFooter.setImportantForAccessibility(enable
+                                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+                            }
+                        });
             }
 
             mLauncher.getWorkspace().beginDragShared(v, this, options);
@@ -299,27 +321,29 @@
     }
 
     public void startEditingFolderName() {
-        post(new Runnable() {
-            @Override
-            public void run() {
-                mFolderName.setHint("");
-                mIsEditingName = true;
+        post(() -> {
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                showLabelSuggestions();
             }
+            mFolderName.setHint("");
+            mIsEditingName = true;
         });
     }
 
-
     @Override
     public boolean onBackKey() {
         // Convert to a string here to ensure that no other state associated with the text field
         // gets saved.
         String newTitle = mFolderName.getText().toString();
-        mInfo.title = newTitle;
+        if (DEBUG) {
+            Log.d(TAG, "onBackKey newTitle=" + newTitle);
+        }
+        mInfo.setTitle(newTitle, mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
-        mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
         if (TextUtils.isEmpty(mInfo.title)) {
             mFolderName.setHint(R.string.folder_hint_text);
+            mFolderName.setText("");
         } else {
             mFolderName.setHint(null);
         }
@@ -338,6 +362,10 @@
     }
 
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (DEBUG) {
+            Log.d(TAG, "onEditorAction actionId=" + actionId + " key="
+                    + (event != null ? event.getKeyCode() : "null event"));
+        }
         if (actionId == EditorInfo.IME_ACTION_DONE) {
             mFolderName.dispatchBackKey();
             return true;
@@ -387,9 +415,11 @@
 
     void bind(FolderInfo info) {
         mInfo = info;
+        mFromTitle = info.title;
+        mFromLabelState = info.getFromLabelState();
         ArrayList<WorkspaceItemInfo> children = info.contents;
         Collections.sort(children, ITEM_POS_COMPARATOR);
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(true);
 
         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
         if (lp == null) {
@@ -400,7 +430,7 @@
         mItemsInvalidated = true;
         mInfo.addListener(this);
 
-        if (!TextUtils.isEmpty(mInfo.title)) {
+        if (!isEmpty(mInfo.title)) {
             mFolderName.setText(mInfo.title);
             mFolderName.setHint(null);
         } else {
@@ -417,17 +447,30 @@
 
 
     /**
-     * Show suggested folder title.
+     * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
+     * rest of the suggestions to InputMethodManager.
      */
-    public void showSuggestedTitle(CharSequence suggestName) {
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && mInfo.contents.size() == 2) {
-            if (!TextUtils.isEmpty(suggestName)) {
-                mFolderName.setHint(suggestName);
-                mFolderName.setText(suggestName);
-                mFolderName.showKeyboard();
-                mInfo.title = suggestName;
+    private void showLabelSuggestions() {
+        if (mInfo.suggestedFolderNames == null) {
+            return;
+        }
+        if (mInfo.suggestedFolderNames.hasSuggestions()) {
+            // update the primary suggestion if the folder name is empty.
+            if (isEmpty(mFolderName.getText())) {
+                if (mInfo.suggestedFolderNames.hasPrimary()) {
+                    mFolderName.setHint("");
+                    mFolderName.setText(mInfo.suggestedFolderNames.getLabels()[0]);
+                    mFolderName.selectAll();
+                }
             }
-            animateOpen();
+            mFolderName.showKeyboard();
+            mFolderName.displayCompletions(
+                    Stream.of(mInfo.suggestedFolderNames.getLabels())
+                            .filter(Objects::nonNull)
+                            .map(Object::toString)
+                            .filter(s -> !s.isEmpty())
+                            .filter(s -> !s.equalsIgnoreCase(mFolderName.getText().toString()))
+                            .collect(Collectors.toList()));
         }
     }
 
@@ -473,6 +516,8 @@
     }
 
     private boolean shouldUseHardwareLayerForAnimation(CellLayout currentCellLayout) {
+        if (ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS.get()) return true;
+
         int folderCount = 0;
         final ShortcutAndWidgetContainer container = currentCellLayout.getShortcutsAndWidgets();
         for (int i = container.getChildCount() - 1; i >= 0; --i) {
@@ -516,6 +561,15 @@
      * is played.
      */
     private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
+        animateOpen(items, pageNo, false);
+    }
+
+    /**
+     * Opens the user folder described by the specified tag. The opening of the folder
+     * is animated relative to the specified View. If the View is null, no animation
+     * is played.
+     */
+    private void animateOpen(List<WorkspaceItemInfo> items, int pageNo, boolean skipUserEventLog) {
         Folder openFolder = getOpen(mLauncher);
         if (openFolder != null && openFolder != this) {
             // Close any open folder before opening a folder.
@@ -536,7 +590,7 @@
             dragLayer.addView(this);
             mDragController.addDropTarget(this);
         } else {
-            if (FeatureFlags.IS_DOGFOOD_BUILD) {
+            if (FeatureFlags.IS_STUDIO_BUILD) {
                 Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
                         + getParent());
             }
@@ -565,10 +619,13 @@
                 mState = STATE_OPEN;
                 announceAccessibilityChanges();
 
-                mLauncher.getUserEventDispatcher().logActionOnItem(
-                        Touch.TAP,
-                        Direction.NONE,
-                        ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+                if (!skipUserEventLog) {
+                    mLauncher.getUserEventDispatcher().logActionOnItem(
+                            LauncherLogProto.Action.Touch.TAP,
+                            LauncherLogProto.Action.Direction.NONE,
+                            LauncherLogProto.ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+                }
+
 
                 mContent.setFocusOnFirstChild();
             }
@@ -653,15 +710,24 @@
     }
 
     private void animateClosed() {
+        if (mIsAnimatingClosed) {
+            return;
+        }
         if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
             mCurrentAnimator.cancel();
         }
         AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator();
         a.addListener(new AnimatorListenerAdapter() {
             @Override
+            public void onAnimationStart(Animator animation) {
+                mIsAnimatingClosed = true;
+            }
+
+            @Override
             public void onAnimationEnd(Animator animation) {
                 closeComplete(true);
                 announceAccessibilityChanges();
+                mIsAnimatingClosed = false;
             }
         });
         startAnimation(a);
@@ -673,6 +739,11 @@
                 : getContext().getString(R.string.folder_closed));
     }
 
+    @Override
+    protected View getAccessibilityInitialFocusView() {
+        return mContent.getFirstItem();
+    }
+
     private void closeComplete(boolean wasAnimated) {
         // TODO: Clear all active animations.
         DragLayer parent = (DragLayer) getParent();
@@ -874,6 +945,7 @@
             View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
                     ? mCurrentDragView : mContent.createNewView(info);
             ArrayList<View> views = getIconsInReadingOrder();
+            info.rank = Utilities.boundToRange(info.rank, 0, views.size());
             views.add(info.rank, icon);
             mContent.arrangeChildren(views);
             mItemsInvalidated = true;
@@ -901,7 +973,7 @@
 
         // Reordering may have occured, and we need to save the new item locations. We do this once
         // at the end to prevent unnecessary database operations.
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(false);
         // Use the item count to check for multi-page as the folder UI may not have
         // been refreshed yet.
         if (getItemCount() <= mContent.itemsPerPage()) {
@@ -911,7 +983,7 @@
         }
     }
 
-    private void updateItemLocationsInDatabaseBatch() {
+    private void updateItemLocationsInDatabaseBatch(boolean isBind) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
                 mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
 
@@ -927,6 +999,16 @@
         if (!items.isEmpty()) {
             mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
         }
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind
+                && total > 1 /* no need to update if there's one icon */) {
+            Executors.MODEL_EXECUTOR.post(() -> {
+                FolderNameInfos nameInfos = new FolderNameInfos();
+                FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
+                fnp.getSuggestedFolderName(
+                        getContext(), mInfo.contents, nameInfos);
+                mInfo.suggestedFolderNames = nameInfos;
+            });
+        }
     }
 
     public void notifyDrop() {
@@ -1068,13 +1150,14 @@
                 int itemCount = getItemCount();
                 if (itemCount <= 1) {
                     View newIcon = null;
+                    WorkspaceItemInfo finalItem = null;
 
                     if (itemCount == 1) {
                         // Move the item from the folder to the workspace, in the position of the
                         // folder
                         CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container,
                                 mInfo.screenId);
-                        WorkspaceItemInfo finalItem = mInfo.contents.remove(0);
+                        finalItem =  mInfo.contents.remove(0);
                         newIcon = mLauncher.createShortcut(cellLayout, finalItem);
                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
                                 mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY);
@@ -1095,6 +1178,9 @@
                         // Focus the newly created child
                         newIcon.requestFocus();
                     }
+                    if (finalItem != null) {
+                        mLauncher.folderConvertedToItem(mFolderIcon.getFolder(), finalItem);
+                    }
                 }
             }
         };
@@ -1227,7 +1313,7 @@
             // We only need to update the locations if it doesn't get handled in
             // #onDropCompleted.
             if (d.dragSource != this) {
-                updateItemLocationsInDatabaseBatch();
+                updateItemLocationsInDatabaseBatch(false);
             }
         }
 
@@ -1243,6 +1329,8 @@
         if (d.stateAnnouncer != null) {
             d.stateAnnouncer.completeAction(R.string.item_moved);
         }
+        mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
+                .log(LAUNCHER_ITEM_DROP_COMPLETED);
     }
 
     // This is used so the item doesn't immediately appear in the folder when added. In one case
@@ -1268,7 +1356,7 @@
         verifier.updateRankAndPos(item, rank);
         mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
                 item.cellY);
-        updateItemLocationsInDatabaseBatch();
+        updateItemLocationsInDatabaseBatch(false);
 
         if (mContent.areViewsBound()) {
             mContent.createAndAddViewForRank(item, rank);
@@ -1341,14 +1429,42 @@
         return itemsOnCurrentPage;
     }
 
+    @Override
     public void onFocusChange(View v, boolean hasFocus) {
         if (v == mFolderName) {
             if (hasFocus) {
+                mFromLabelState = mInfo.getFromLabelState();
+                mFromTitle = mInfo.title;
                 startEditingFolderName();
             } else {
-                if (isEditingName()) {
-                    logEditFolderLabel();
+                StatsLogger statsLogger = mStatsLogManager.logger()
+                        .withItemInfo(mInfo)
+                        .withFromState(mFromLabelState);
+
+                // If the folder label is suggested, it is logged to improve prediction model.
+                // When both old and new labels are logged together delimiter is used.
+                StringJoiner labelInfoBuilder = new StringJoiner(FOLDER_LABEL_DELIMITER);
+                if (mFromLabelState.equals(FromState.FROM_SUGGESTED)) {
+                    labelInfoBuilder.add(mFromTitle);
                 }
+
+                ToState toLabelState;
+                if (mFromTitle != null && mFromTitle.equals(mInfo.title)) {
+                    toLabelState = ToState.UNCHANGED;
+                } else {
+                    toLabelState = mInfo.getToLabelState();
+                    if (toLabelState.toString().startsWith("TO_SUGGESTION")) {
+                        labelInfoBuilder.add(mInfo.title);
+                    }
+                }
+                statsLogger.withToState(toLabelState);
+
+                if (labelInfoBuilder.length() > 0) {
+                    statsLogger.withEditText(labelInfoBuilder.toString());
+                }
+
+                statsLogger.log(LAUNCHER_FOLDER_LABEL_UPDATED);
+                logFolderLabelState(mFromLabelState, toLabelState);
                 mFolderName.dispatchBackKey();
             }
         }
@@ -1362,11 +1478,24 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        target.gridX = info.cellX;
-        target.gridY = info.cellY;
-        target.pageIndex = mContent.getCurrentPage();
-        targetParent.containerType = ContainerType.FOLDER;
+    public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
+            ArrayList<LauncherLogProto.Target> targets) {
+        child.gridX = childInfo.cellX;
+        child.gridY = childInfo.cellY;
+        child.pageIndex = mContent.getCurrentPage();
+
+        LauncherLogProto.Target target = newContainerTarget(LauncherLogProto.ContainerType.FOLDER);
+        target.pageIndex = mInfo.screenId;
+        target.gridX = mInfo.cellX;
+        target.gridY = mInfo.cellY;
+        targets.add(target);
+
+        // continue to parent
+        if (mInfo.container == CONTAINER_HOTSEAT) {
+            mLauncher.getHotseat().fillInLogContainerData(mInfo, target, targets);
+        } else {
+            mLauncher.getWorkspace().fillInLogContainerData(mInfo, target, targets);
+        }
     }
 
     private class OnScrollHintListener implements OnAlarmListener {
@@ -1464,9 +1593,12 @@
 
     @Override
     public int getLogContainerType() {
-        return ContainerType.FOLDER;
+        return LauncherLogProto.ContainerType.FOLDER;
     }
 
+    /**
+     * Navigation bar back key or hardware input back key has been issued.
+     */
     @Override
     public boolean onBackPressed() {
         if (isEditingName()) {
@@ -1496,7 +1628,7 @@
                     }
                 } else {
                     mLauncher.getUserEventDispatcher().logActionTapOutside(
-                            LoggerUtils.newContainerTarget(ContainerType.FOLDER));
+                            newContainerTarget(LauncherLogProto.ContainerType.FOLDER));
                     close(true);
                     return true;
                 }
@@ -1527,38 +1659,18 @@
         }
     }
 
-    private void logEditFolderLabel() {
-        LauncherLogProto.LauncherEvent ev = new LauncherLogProto.LauncherEvent();
-        LauncherLogProto.Action action = new LauncherLogProto.Action();
-        action.type = LauncherLogProto.Action.Type.SOFT_KEYBOARD;
-        ev.action = action;
+    public FolderPagedView getContent() {
+        return mContent;
+    }
 
-        LauncherLogProto.Target edittext_target = new LauncherLogProto.Target();
-        edittext_target.type = LauncherLogProto.Target.Type.ITEM;
-        edittext_target.itemType = LauncherLogProto.ItemType.EDITTEXT;
-
-        LauncherLogProto.Target folder_target = new LauncherLogProto.Target();
-        folder_target.type = LauncherLogProto.Target.Type.CONTAINER;
-        folder_target.containerType = LauncherLogProto.ContainerType.FOLDER;
-        folder_target.pageIndex = mInfo.screenId;
-        folder_target.gridX = mInfo.cellX;
-        folder_target.gridY = mInfo.cellY;
-        folder_target.cardinality = mInfo.contents.size();
-
-        LauncherLogProto.Target parent_target = new LauncherLogProto.Target();
-        parent_target.type = LauncherLogProto.Target.Type.CONTAINER;
-        switch (mInfo.container) {
-            case CONTAINER_HOTSEAT:
-                parent_target.containerType = LauncherLogProto.ContainerType.HOTSEAT;
-                break;
-            case CONTAINER_DESKTOP:
-                parent_target.containerType = LauncherLogProto.ContainerType.WORKSPACE;
-                break;
-            default:
-                Log.e(TAG, String.format("Expected container to be either %s or %s but found %s.",
-                        CONTAINER_HOTSEAT, CONTAINER_DESKTOP, mInfo.container));
-        }
-        ev.srcTarget = new LauncherLogProto.Target[]{edittext_target, folder_target, parent_target};
-        mLauncher.getUserEventDispatcher().dispatchUserEvent(ev, null);
+    /**
+     * Logs current folder label info.
+     *
+     * @deprecated This method is only used for log validation and soon will be removed.
+     */
+    @Deprecated
+    public void logFolderLabelState(FromState fromState, ToState toState) {
+        mLauncher.getUserEventDispatcher()
+                .logLauncherEvent(mInfo.getFolderLabelStateLauncherEvent(fromState, toState));
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 1310d37..3d72b49 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -115,6 +115,7 @@
      */
     public AnimatorSet getAnimator() {
         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
+        mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams();
         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
 
@@ -220,7 +221,6 @@
         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
         play(a, z, mIsOpening ? midDuration : 0, midDuration);
 
-
         // Store clip variables
         CellLayout cellLayout = mContent.getCurrentCellLayout();
         boolean folderClipChildren = mFolder.getClipChildren();
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 9d14a5f..4be82ed 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -20,9 +20,9 @@
 
 import android.graphics.Point;
 
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index fd6d1e3..75275b2 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -18,12 +18,16 @@
 
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
@@ -31,7 +35,6 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -39,34 +42,41 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.Alarm;
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.FolderInfo.FolderListener;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.OnAlarmListener;
 import com.android.launcher3.R;
-import com.android.launcher3.SimpleOnStylusPressListener;
-import com.android.launcher3.StylusEventHelper;
+import com.android.launcher3.Reorderable;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.BaseItemDragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.FolderInfo.FolderListener;
+import com.android.launcher3.model.data.FolderInfo.LabelState;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.IconLabelDotView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
@@ -74,17 +84,18 @@
 import java.util.List;
 import java.util.function.Predicate;
 
+
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
-public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView {
+public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
+        DraggableView, Reorderable {
 
-    @Thunk Launcher mLauncher;
+    @Thunk ActivityContext mActivity;
     @Thunk Folder mFolder;
-    private FolderInfo mInfo;
+    public FolderInfo mInfo;
 
     private CheckLongPressHelper mLongPressHelper;
-    private StylusEventHelper mStylusEventHelper;
 
     static final int DROP_IN_ANIMATION_DURATION = 400;
 
@@ -107,8 +118,6 @@
 
     boolean mAnimating = false;
 
-    private float mSlop;
-
     private Alarm mOpenAlarm = new Alarm();
 
     private boolean mForceHideDot;
@@ -120,6 +129,10 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+    private float mScaleForReorderBounce = 1f;
+
     private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY
             = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") {
         @Override
@@ -146,14 +159,26 @@
 
     private void init() {
         mLongPressHelper = new CheckLongPressHelper(this);
-        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
         mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
-        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
         mPreviewItemManager = new PreviewItemManager(this);
         mDotParams = new DotRenderer.DrawParams();
     }
 
-    public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+    public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
+            FolderInfo folderInfo) {
+        Folder folder = Folder.fromXml(launcher);
+        folder.setDragController(launcher.getDragController());
+
+        FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+        folder.setFolderIcon(icon);
+        folder.bind(folderInfo);
+        icon.setFolder(folder);
+
+        icon.setOnFocusChangeListener(launcher.getFocusHandler());
+        return icon;
+    }
+
+    public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
             FolderInfo folderInfo) {
         @SuppressWarnings("all") // suppress dead code warning
         final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
@@ -163,7 +188,7 @@
                     "is dependent on this");
         }
 
-        DeviceProfile grid = launcher.getWallpaperDeviceProfile();
+        DeviceProfile grid = activity.getDeviceProfile();
         FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
                 .inflate(resId, group, false);
 
@@ -177,19 +202,26 @@
         icon.setTag(folderInfo);
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
         icon.mInfo = folderInfo;
-        icon.mLauncher = launcher;
+        icon.mActivity = activity;
         icon.mDotRenderer = grid.mDotRendererWorkSpace;
-        icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
-        Folder folder = Folder.fromXml(launcher);
-        folder.setDragController(launcher.getDragController());
-        folder.setFolderIcon(icon);
-        folder.bind(folderInfo);
-        icon.setFolder(folder);
-        icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
+
+        icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title));
+
+        // Keep the notification dot up to date with the sum of all the content's dots.
+        FolderDotInfo folderDotInfo = new FolderDotInfo();
+        for (WorkspaceItemInfo si : folderInfo.contents) {
+            folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
+        }
+        icon.setDotInfo(folderDotInfo);
+
+        icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+        icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
+        icon.mPreviewVerifier.setFolderInfo(folderInfo);
+        icon.updatePreviewItems(false);
 
         folderInfo.addListener(icon);
 
-        icon.setOnFocusChangeListener(launcher.mFocusHandler);
         return icon;
     }
 
@@ -217,9 +249,6 @@
 
     private void setFolder(Folder folder) {
         mFolder = folder;
-        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
-        mPreviewVerifier.setFolderInfo(mFolder.getInfo());
-        updatePreviewItems(false);
     }
 
     private boolean willAcceptItem(ItemInfo item) {
@@ -268,8 +297,9 @@
     }
 
     public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView,
-            final WorkspaceItemInfo srcInfo, final DragView srcView, Rect dstRect,
+            final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect,
             float scaleRelativeToDragLayer) {
+        final DragView srcView = d.dragView;
         prepareCreateAnimation(destView);
         addItem(destInfo);
         // This will animate the first item from it's position as an icon into its
@@ -278,7 +308,7 @@
                 .start();
 
         // This will animate the dragView (srcView) into the new folder
-        onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1,
+        onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1,
                 false /* itemReturnedOnFailedDrop */);
     }
 
@@ -293,22 +323,23 @@
         mOpenAlarm.cancelAlarm();
     }
 
-    private void onDrop(final WorkspaceItemInfo item, DragView animateView, Rect finalRect,
+    private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect,
             float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) {
         item.cellX = -1;
         item.cellY = -1;
-
+        DragView animateView = d.dragView;
         // Typically, the animateView corresponds to the DragView; however, if this is being done
         // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
         // will not have a view to animate
-        if (animateView != null) {
-            DragLayer dragLayer = mLauncher.getDragLayer();
+        if (animateView != null && mActivity instanceof Launcher) {
+            final Launcher launcher = (Launcher) mActivity;
+            DragLayer dragLayer = launcher.getDragLayer();
             Rect from = new Rect();
             dragLayer.getViewRectRelativeToSelf(animateView, from);
             Rect to = finalRect;
             if (to == null) {
                 to = new Rect();
-                Workspace workspace = mLauncher.getWorkspace();
+                Workspace workspace = launcher.getWorkspace();
                 // Set cellLayout and this to it's final state to compute final animation locations
                 workspace.setFinalTransitionTransform();
                 float scaleX = getScaleX();
@@ -361,6 +392,14 @@
             float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f;
 
             float finalScale = scale * scaleRelativeToDragLayer;
+
+            // Account for potentially different icon sizes with non-default grid settings
+            if (d.dragSource instanceof AllAppsContainerView) {
+                DeviceProfile grid = mActivity.getDeviceProfile();
+                float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx);
+                finalScale *= containerScale;
+            }
+
             dragLayer.animateView(animateView, from, to, finalAlpha,
                     1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
                     Interpolators.DEACCEL_2, Interpolators.ACCEL_2,
@@ -371,22 +410,76 @@
             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
             final int finalIndex = index;
 
-            String[] suggestedNameOut = new String[1];
+            FolderNameInfos nameInfos = new FolderNameInfos();
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                Executors.UI_HELPER_EXECUTOR.post(() -> mLauncher.getFolderNameProvider()
-                        .getSuggestedFolderName(getContext(), mInfo.contents, suggestedNameOut));
+                Executors.MODEL_EXECUTOR.post(() -> {
+                    d.folderNameProvider.getSuggestedFolderName(
+                            getContext(), mInfo.contents, nameInfos);
+                    showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
+                });
+            } else {
+                showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
             }
-            postDelayed(() -> {
-                mPreviewItemManager.hidePreviewItem(finalIndex, false);
-                mFolder.showItem(item);
-                invalidate();
-                mFolder.showSuggestedTitle(suggestedNameOut[0]);
-            }, DROP_IN_ANIMATION_DURATION);
         } else {
             addItem(item);
         }
     }
 
+    private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
+            FolderNameInfos nameInfos, InstanceId instanceId) {
+        postDelayed(() -> {
+            mPreviewItemManager.hidePreviewItem(finalIndex, false);
+            mFolder.showItem(item);
+            setLabelSuggestion(nameInfos, instanceId);
+            invalidate();
+        }, DROP_IN_ANIMATION_DURATION);
+    }
+
+    /**
+     * Set the suggested folder name.
+     */
+    public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) {
+        if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            return;
+        }
+        if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) {
+            return;
+        }
+        if (nameInfos == null || !nameInfos.hasSuggestions()) {
+            StatsLogManager.newInstance(getContext()).logger()
+                    .withInstanceId(instanceId)
+                    .withItemInfo(mInfo)
+                    .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS);
+            return;
+        }
+        if (!nameInfos.hasPrimary()) {
+            StatsLogManager.newInstance(getContext()).logger()
+                    .withInstanceId(instanceId)
+                    .withItemInfo(mInfo)
+                    .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY);
+            return;
+        }
+        CharSequence newTitle = nameInfos.getLabels()[0];
+        FromState fromState = mInfo.getFromLabelState();
+
+        mInfo.setTitle(newTitle, mFolder.mLauncher.getModelWriter());
+        onTitleChanged(mInfo.title);
+        mFolder.mFolderName.setText(mInfo.title);
+
+        // Logging for folder creation flow
+        StatsLogManager.newInstance(getContext()).logger()
+                .withInstanceId(instanceId)
+                .withItemInfo(mInfo)
+                .withFromState(fromState)
+                .withToState(ToState.TO_SUGGESTION0)
+                // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter,
+                // event is assumed to be folder creation on the server side.
+                .withEditText(newTitle.toString())
+                .log(LAUNCHER_FOLDER_AUTO_LABELED);
+        mFolder.logFolderLabelState(fromState, ToState.TO_SUGGESTION0);
+    }
+
+
     public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) {
         WorkspaceItemInfo item;
         if (d.dragInfo instanceof AppInfo) {
@@ -399,9 +492,10 @@
             item = (WorkspaceItemInfo) d.dragInfo;
         }
         mFolder.notifyDrop();
-        onDrop(item, d.dragView, null, 1.0f,
+        onDrop(item, d, null, 1.0f,
                 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
-                itemReturnedOnFailedDrop);
+                itemReturnedOnFailedDrop
+        );
     }
 
     public void setDotInfo(FolderDotInfo dotInfo) {
@@ -492,6 +586,10 @@
         invalidate();
     }
 
+    public boolean getIconVisible() {
+        return mBackgroundIsVisible;
+    }
+
     public PreviewBackground getFolderBackground() {
         return mBackground;
     }
@@ -529,8 +627,7 @@
     public void drawDot(Canvas canvas) {
         if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
             Rect iconBounds = mDotParams.iconBounds;
-            BubbleTextView.getIconBounds(this, iconBounds,
-                    mLauncher.getWallpaperDeviceProfile().iconSizePx);
+            BubbleTextView.getIconBounds(this, iconBounds, mActivity.getDeviceProfile().iconSizePx);
             float iconScale = (float) mBackground.previewSize / iconBounds.width();
             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
 
@@ -588,9 +685,10 @@
     @Override
     public void onAdd(WorkspaceItemInfo item, int rank) {
         boolean wasDotted = mDotInfo.hasDot();
-        mDotInfo.addDotInfo(mLauncher.getDotInfoForItem(item));
+        mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
         boolean isDotted = mDotInfo.hasDot();
         updateDotScale(wasDotted, isDotted);
+        setContentDescription(getAccessiblityTitle(mInfo.title));
         invalidate();
         requestLayout();
     }
@@ -598,45 +696,27 @@
     @Override
     public void onRemove(WorkspaceItemInfo item) {
         boolean wasDotted = mDotInfo.hasDot();
-        mDotInfo.subtractDotInfo(mLauncher.getDotInfoForItem(item));
+        mDotInfo.subtractDotInfo(mActivity.getDotInfoForItem(item));
         boolean isDotted = mDotInfo.hasDot();
         updateDotScale(wasDotted, isDotted);
+        setContentDescription(getAccessiblityTitle(mInfo.title));
         invalidate();
         requestLayout();
     }
 
     public void onTitleChanged(CharSequence title) {
         mFolderName.setText(title);
-        setContentDescription(getContext().getString(R.string.folder_name_format, title));
+        setContentDescription(getAccessiblityTitle(title));
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
-        boolean result = super.onTouchEvent(event);
-
-        // Check for a stylus button press, if it occurs cancel any long press checks.
-        if (mStylusEventHelper.onMotionEvent(event)) {
-            mLongPressHelper.cancelLongPress();
-            return true;
-        }
-
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mLongPressHelper.postCheckForLongPress();
-                break;
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                mLongPressHelper.cancelLongPress();
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
-                    mLongPressHelper.cancelLongPress();
-                }
-                break;
-        }
-        return result;
+        super.onTouchEvent(event);
+        mLongPressHelper.onTouchEvent(event);
+        // Keep receiving the rest of the events
+        return true;
     }
 
     @Override
@@ -671,4 +751,66 @@
     public void onFolderClose(int currentPage) {
         mPreviewItemManager.onFolderClose(currentPage);
     }
+
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
+    }
+
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
+    }
+
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
+        super.setScaleX(scale);
+        super.setScaleY(scale);
+    }
+
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getWorkspaceVisualDragBounds(Rect bounds) {
+        getPreviewBounds(bounds);
+    }
+
+    /**
+     * Returns a formatted accessibility title for folder
+     */
+    public String getAccessiblityTitle(CharSequence title) {
+        int size = mInfo.contents.size();
+        if (size < MAX_NUM_ITEMS_IN_PREVIEW) {
+            return getContext().getString(R.string.folder_name_format_exact, title, size);
+        } else {
+            return getContext().getString(R.string.folder_name_format_overflow, title,
+                    MAX_NUM_ITEMS_IN_PREVIEW);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderNameEditText.java b/src/com/android/launcher3/folder/FolderNameEditText.java
new file mode 100644
index 0000000..6038a05
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderNameEditText.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.launcher3.ExtendedEditText;
+
+import java.util.List;
+
+/**
+ * Handles additional edit text functionality to better support folder name suggestion.
+ * First, makes suggestion to the InputMethodManager via {@link #displayCompletions(List)}
+ * Second, intercepts whether user accepted the suggestion or manually edited their
+ * folder names.
+ */
+public class FolderNameEditText extends ExtendedEditText {
+    private static final String TAG = "FolderNameEditText";
+    private static final boolean DEBUG = false;
+
+    private boolean mEnteredCompose = false;
+
+    public FolderNameEditText(Context context) {
+        super(context);
+    }
+
+    public FolderNameEditText(Context context, AttributeSet attrs) {
+        // ctor chaining breaks the touch handling
+        super(context, attrs);
+    }
+
+    public FolderNameEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        InputConnection con = super.onCreateInputConnection(outAttrs);
+        FolderNameEditTextInputConnection connectionWrapper =
+                new FolderNameEditTextInputConnection(con, true);
+        return connectionWrapper;
+    }
+
+    /**
+     * Send strings in @param suggestList to the IME to show up as suggestions.
+     */
+    public void displayCompletions(List<String> suggestList) {
+        int cnt = Math.min(suggestList.size(), FolderNameProvider.SUGGEST_MAX);
+        CompletionInfo[] cInfo = new CompletionInfo[cnt];
+        for (int i = 0; i < cnt; i++) {
+            cInfo[i] = new CompletionInfo(i, i, suggestList.get(i));
+        }
+        // post it to future frame so that onSelectionChanged, onFocusChanged, all other
+        // TextView flag change and IME animation has settled. Ideally, there should be IMM
+        // callback to notify when the IME animation and state handling is finished.
+        postDelayed(() -> getContext().getSystemService(InputMethodManager.class)
+                .displayCompletions(this, cInfo), 40 /* 2~3 frame delay */);
+    }
+
+    /**
+     * Within 's', the 'count' characters beginning at 'start' have just replaced
+     * old text 'before'
+     */
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        String reason = "unknown";
+        if (start == 0 && count == 0 && before > 0) {
+            reason = "suggestion was rejected";
+            mEnteredCompose = true;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "onTextChanged " + start + "," + before + "," + count
+                    + ", " + reason);
+        }
+    }
+
+    @Override
+    public void onCommitCompletion(CompletionInfo text) {
+        setText(text.getText());
+        setSelection(text.getText().length());
+        mEnteredCompose = false;
+    }
+
+    protected void setEnteredCompose(boolean value) {
+        mEnteredCompose = value;
+    }
+
+    private class FolderNameEditTextInputConnection extends InputConnectionWrapper {
+
+        FolderNameEditTextInputConnection(InputConnection target, boolean mutable) {
+            super(target, mutable);
+        }
+
+        @Override
+        public boolean setComposingText(CharSequence cs, int newCursorPos) {
+            mEnteredCompose = true;
+            return super.setComposingText(cs, newCursorPos);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderNameInfos.java b/src/com/android/launcher3/folder/FolderNameInfos.java
new file mode 100644
index 0000000..457ae87
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderNameInfos.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Information about a  label suggestions of a Folder.
+ */
+
+public class FolderNameInfos {
+    public static final int SUCCESS = 1;
+    public static final int HAS_PRIMARY = 1 << 1;
+    public static final int HAS_SUGGESTIONS = 1 << 2;
+    public static final int ERROR_NO_PROVIDER = 1 << 3;
+    public static final int ERROR_APP_LOOKUP_FAILED = 1 << 4;
+    public static final int ERROR_ALL_APP_LOOKUP_FAILED = 1 << 5;
+    public static final int ERROR_NO_LABELS_GENERATED = 1 << 6;
+    public static final int ERROR_LABEL_LOOKUP_FAILED = 1 << 7;
+    public static final int ERROR_ALL_LABEL_LOOKUP_FAILED = 1 << 8;
+    public static final int ERROR_NO_PACKAGES = 1 << 9;
+
+    private int mStatus;
+    private final CharSequence[] mLabels;
+    private final Float[] mScores;
+
+    public FolderNameInfos() {
+        mStatus = 0;
+        mLabels = new CharSequence[FolderNameProvider.SUGGEST_MAX];
+        mScores = new Float[FolderNameProvider.SUGGEST_MAX];
+    }
+
+    /**
+     * set the status of FolderNameInfos.
+     */
+    public void setStatus(int statusBit) {
+        mStatus = mStatus | statusBit;
+    }
+
+    /**
+     * returns status of FolderNameInfos generations.
+     */
+    public int status() {
+        return mStatus;
+    }
+
+    /**
+     * return true if the first suggestion is a Primary suggestion.
+     */
+    public boolean hasPrimary() {
+        return (mStatus & HAS_PRIMARY) > 0 && (mLabels[0] != null);
+    }
+
+    /**
+     * return true if there is at least one valid suggestion.
+     */
+    public boolean hasSuggestions() {
+        for (CharSequence l : mLabels) {
+            if (l != null && !TextUtils.isEmpty(l)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * assign label and score in the specified index.
+     */
+    public void setLabel(int index, CharSequence label, Float score) {
+        if (index < mLabels.length) {
+            mLabels[index] = label;
+            mScores[index] = score;
+        }
+    }
+
+    /**
+     * returns true if the label is found in label suggestions/
+     */
+    public boolean contains(CharSequence label) {
+        return Arrays.stream(mLabels)
+                .filter(Objects::nonNull)
+                .anyMatch(l -> l.toString().equalsIgnoreCase(label.toString()));
+    }
+
+
+    public CharSequence[] getLabels() {
+        return mLabels;
+    }
+
+    public Float[] getScores() {
+        return mScores;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return String.format("status=%s, labels=%s", Integer.toBinaryString(mStatus),
+                Arrays.toString(mLabels));
+    }
+}
+
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 0a1221e..d166e27 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -17,45 +17,166 @@
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
 
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Locates provider for the folder name.
  */
-public class FolderNameProvider {
+public class FolderNameProvider implements ResourceBasedOverride {
+
+    private static final String TAG = "FolderNameProvider";
+    private static final boolean DEBUG = true;
 
     /**
-     * Returns suggested folder name.
+     * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
+     * name edit box can also be used to provide suggestion.
      */
-    public CharSequence getSuggestedFolderName(Context context,
-            ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
-        // Currently only run the algorithm on initial folder creation.
-        // For more than 2 items in the folder, the ranking algorithm for finding
-        // candidate folder name should be rewritten.
-        if (workspaceItemInfos.size() == 2) {
-            ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
-            ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
+    public static final int SUGGEST_MAX = 4;
+    protected IntSparseArrayMap<FolderInfo> mFolderInfos;
+    protected List<AppInfo> mAppInfos;
 
-            String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
-            String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
-            // If the two icons are from the same package,
-            // then assign the main icon's name
-            if (pkgName0.equals(pkgName1)) {
-                WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
-                WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
-                if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo0.title;
-                } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo1.title;
-                }
-                return suggestName[0];
-                // two icons are all shortcuts. Don't assign title
+    /**
+     * Retrieve instance of this object that can be overridden in runtime based on the build
+     * variant of the application.
+     */
+    public static FolderNameProvider newInstance(Context context) {
+        FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class,
+                context.getApplicationContext(), R.string.folder_name_provider_class);
+        Preconditions.assertWorkerThread();
+        fnp.load(context);
+
+        return fnp;
+    }
+
+    public static FolderNameProvider newInstance(Context context, List<AppInfo> appInfos,
+            IntSparseArrayMap<FolderInfo> folderInfos) {
+        Preconditions.assertWorkerThread();
+        FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class,
+                context.getApplicationContext(), R.string.folder_name_provider_class);
+        fnp.load(appInfos, folderInfos);
+
+        return fnp;
+    }
+
+    private void load(Context context) {
+        LauncherAppState.getInstance(context).getModel().enqueueModelUpdateTask(
+                new FolderNameWorker());
+    }
+
+    private void load(List<AppInfo> appInfos, IntSparseArrayMap<FolderInfo> folderInfos) {
+        mAppInfos = appInfos;
+        mFolderInfos = folderInfos;
+    }
+
+    /**
+     * Generate and rank the suggested Folder names.
+     */
+    public void getSuggestedFolderName(Context context,
+            ArrayList<WorkspaceItemInfo> workspaceItemInfos,
+            FolderNameInfos nameInfos) {
+        Preconditions.assertWorkerThread();
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
+        }
+        // If all the icons are from work profile,
+        // Then, suggest "Work" as the folder name
+        Set<UserHandle> users = workspaceItemInfos.stream().map(w -> w.user)
+                .collect(Collectors.toSet());
+        if (users.size() == 1 && !users.contains(Process.myUserHandle())) {
+            setAsLastSuggestion(nameInfos,
+                    context.getResources().getString(R.string.work_folder_name));
+        }
+
+        // If all the icons are from same package (e.g., main icon, shortcut, shortcut)
+        // Then, suggest the package's title as the folder name
+        Set<String> packageNames = workspaceItemInfos.stream()
+                .map(WorkspaceItemInfo::getTargetComponent)
+                .filter(Objects::nonNull)
+                .map(ComponentName::getPackageName)
+                .collect(Collectors.toSet());
+
+        if (packageNames.size() == 1) {
+            Optional<AppInfo> info = getAppInfoByPackageName(packageNames.iterator().next());
+            // Place it as first viable suggestion and shift everything else
+            info.ifPresent(i -> setAsFirstSuggestion(nameInfos, i.title.toString()));
+        }
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
+        }
+    }
+
+    private Optional<AppInfo> getAppInfoByPackageName(String packageName) {
+        if (mAppInfos == null || mAppInfos.isEmpty()) {
+            return Optional.empty();
+        }
+        return mAppInfos.stream()
+                .filter(info -> info.componentName != null)
+                .filter(info -> info.componentName.getPackageName().equals(packageName))
+                .findAny();
+    }
+
+    private void setAsFirstSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
+            return;
+        }
+        nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY);
+        nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS);
+        CharSequence[] labels = nameInfos.getLabels();
+        Float[] scores = nameInfos.getScores();
+        for (int i = labels.length - 1; i > 0; i--) {
+            if (labels[i - 1] != null && !TextUtils.isEmpty(labels[i - 1])) {
+                nameInfos.setLabel(i, labels[i - 1], scores[i - 1]);
             }
         }
-        return suggestName[0];
+        nameInfos.setLabel(0, label, 1.0f);
     }
+
+    private void setAsLastSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
+            return;
+        }
+        nameInfos.setStatus(FolderNameInfos.HAS_PRIMARY);
+        nameInfos.setStatus(FolderNameInfos.HAS_SUGGESTIONS);
+        CharSequence[] labels = nameInfos.getLabels();
+        for (int i = 0; i < labels.length; i++) {
+            if (labels[i] == null || TextUtils.isEmpty(labels[i])) {
+                nameInfos.setLabel(i, label, 1.0f);
+                return;
+            }
+        }
+        // Overwrite the last suggestion.
+        nameInfos.setLabel(labels.length - 1, label, 1.0f);
+    }
+
+    private class FolderNameWorker extends BaseModelUpdateTask {
+        @Override
+        public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+            mFolderInfos = dataModel.folders.clone();
+            mAppInfos = Arrays.asList(apps.copyData());
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 3b5fd59..32531c0 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
+
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -27,12 +30,12 @@
 import android.view.View;
 import android.view.ViewDebug;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.PagedView;
@@ -40,9 +43,10 @@
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace.ItemOperator;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Thunk;
@@ -149,6 +153,7 @@
             CellLayout page = (CellLayout) getChildAt(i);
             ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
             for (int j = container.getChildCount() - 1; j >= 0; j--) {
+                container.getChildAt(j).setVisibility(View.VISIBLE);
                 mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
             }
             page.removeAllViews();
@@ -258,7 +263,7 @@
     @Override
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
         super.onScrollChanged(l, t, oldl, oldt);
-        mPageIndicator.setScroll(l, mMaxScrollX);
+        if (mMaxScroll > 0) mPageIndicator.setScroll(l, mMaxScroll);
     }
 
     /**
@@ -614,6 +619,12 @@
         }
     }
 
+    @Override
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        return AbstractFloatingView.getTopOpenViewWithType(mFolder.mLauncher,
+                TYPE_ALL & ~TYPE_FOLDER) == null;
+    }
+
     public int itemsPerPage() {
         return mOrganizer.getMaxItemsPerPage();
     }
diff --git a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
index 1e56f7d..22f7333 100644
--- a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
+++ b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
@@ -44,7 +44,8 @@
                 }
             };
 
-    private static PreviewItemDrawingParams sTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+    private static final PreviewItemDrawingParams sTmpParams =
+            new PreviewItemDrawingParams(0, 0, 0, 0);
     private static final float[] sTempParamsArray = new float[3];
 
     private final ObjectAnimator mAnimator;
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index b2c0ca7..27b906b 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -48,7 +48,7 @@
  * This object represents a FolderIcon preview background. It stores drawing / measurement
  * information, handles drawing, and animation (accept state <--> rest state).
  */
-public class PreviewBackground {
+public class PreviewBackground extends CellLayout.DelegatedCellDrawing {
 
     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
 
@@ -76,8 +76,6 @@
     int basePreviewOffsetY;
 
     private CellLayout mDrawingDelegate;
-    public int delegateCellX;
-    public int delegateCellY;
 
     // When the PreviewBackground is drawn under an icon (for creating a folder) the border
     // should not occlude the icon
@@ -124,6 +122,27 @@
                 }
             };
 
+    /**
+     * Draws folder background under cell layout
+     */
+    @Override
+    public void drawUnderItem(Canvas canvas) {
+        drawBackground(canvas);
+        if (!isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
+    /**
+     * Draws folder background on cell layout
+     */
+    @Override
+    public void drawOverItem(Canvas canvas) {
+        if (isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
     public void setup(Context context, ActivityContext activity, View invalidateDelegate,
                       int availableSpaceX, int topPadding) {
         mInvalidateDelegate = invalidateDelegate;
@@ -134,7 +153,7 @@
         mBgColor = ta.getColor(R.styleable.FolderIconPreview_folderFillColor, 0);
         ta.recycle();
 
-        DeviceProfile grid = activity.getWallpaperDeviceProfile();
+        DeviceProfile grid = activity.getDeviceProfile();
         previewSize = grid.folderIconSizePx;
 
         basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
@@ -317,19 +336,19 @@
 
     private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
         if (mDrawingDelegate != delegate) {
-            delegate.addFolderBackground(this);
+            delegate.addDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = delegate;
-        delegateCellX = cellX;
-        delegateCellY = cellY;
+        mDelegateCellX = cellX;
+        mDelegateCellY = cellY;
 
         invalidate();
     }
 
     private void clearDrawingDelegate() {
         if (mDrawingDelegate != null) {
-            mDrawingDelegate.removeFolderBackground(this);
+            mDrawingDelegate.removeDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = null;
@@ -395,8 +414,8 @@
         // is saved and restored at the beginning of the animation, since cancelling the
         // existing animation can clear the delgate.
         CellLayout cl = mDrawingDelegate;
-        int cellX = delegateCellX;
-        int cellY = delegateCellY;
+        int cellX = mDelegateCellX;
+        int cellY = mDelegateCellY;
         animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
     }
 
diff --git a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
index caf6e55..a14a0d8 100644
--- a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
+++ b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
@@ -17,7 +17,7 @@
 
 import android.graphics.drawable.Drawable;
 
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 /**
  * Manages the parameters used to draw a Folder preview item.
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 2d817e6..7f8a15c 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -16,10 +16,12 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -35,11 +37,10 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -66,7 +67,6 @@
 
     private final Context mContext;
     private final FolderIcon mIcon;
-    private final DrawableFactory mDrawableFactory;
     private final int mIconSize;
 
     // These variables are all associated with the drawing of the preview; they are stored
@@ -94,8 +94,8 @@
     public PreviewItemManager(FolderIcon icon) {
         mContext = icon.getContext();
         mIcon = icon;
-        mDrawableFactory = DrawableFactory.INSTANCE.get(mContext);
-        mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
+        mIconSize = ActivityContext.lookupContext(
+                mContext).getDeviceProfile().folderChildIconSizePx;
     }
 
     /**
@@ -133,7 +133,7 @@
             mTotalWidth = totalSize;
             mPrevTopPadding = mIcon.getPaddingTop();
 
-            mIcon.mBackground.setup(mIcon.mLauncher, mIcon.mLauncher, mIcon, mTotalWidth,
+            mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
                     mIcon.getPaddingTop());
             mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
                     Utilities.isRtl(mIcon.getResources()));
@@ -153,7 +153,7 @@
     }
 
     private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
-        float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx;
+        float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
 
         final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
         final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
@@ -395,11 +395,11 @@
 
     private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi()) {
-            PreloadIconDrawable drawable = mDrawableFactory.newPendingIcon(mContext, item);
+            PreloadIconDrawable drawable = newPendingIcon(mContext, item);
             drawable.setLevel(item.getInstallProgress());
             p.drawable = drawable;
         } else {
-            p.drawable = mDrawableFactory.newIcon(mContext, item);
+            p.drawable = newIcon(mContext, item);
         }
         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         p.item = item;
diff --git a/src/com/android/launcher3/graphics/BitmapCreationCheck.java b/src/com/android/launcher3/graphics/BitmapCreationCheck.java
new file mode 100644
index 0000000..e63e542
--- /dev/null
+++ b/src/com/android/launcher3/graphics/BitmapCreationCheck.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.graphics;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Application;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.GraphicsUtils;
+
+/**
+ * Utility class to check bitmap creation during draw pass.
+ */
+public class BitmapCreationCheck {
+
+    private static final String TAG = "BitmapCreationCheck";
+
+    public static final boolean ENABLED = false;
+
+    /**
+     * Starts tracking bitmap creations during {@link View#draw(Canvas)} calls
+     */
+    public static void startTracking(Context context) {
+        MyTracker tracker = new MyTracker();
+        ((Application) context.getApplicationContext()).registerActivityLifecycleCallbacks(tracker);
+        GraphicsUtils.sOnNewBitmapRunnable = tracker::onBitmapCreated;
+    }
+
+    @TargetApi(VERSION_CODES.Q)
+    private static class MyTracker
+            implements ActivityLifecycleCallbacks, OnAttachStateChangeListener {
+
+        private final ThreadLocal<Boolean> mCurrentThreadDrawing =
+                ThreadLocal.withInitial(() -> false);
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle bundle) {
+            activity.getWindow().getDecorView().addOnAttachStateChangeListener(this);
+        }
+
+        @Override
+        public void onActivityStarted(Activity activity) { }
+
+        @Override
+        public void onActivityResumed(Activity activity) { }
+
+        @Override
+        public void onActivityPaused(Activity activity) { }
+
+        @Override
+        public void onActivityStopped(Activity activity) { }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
+
+        @Override
+        public void onActivityDestroyed(Activity activity) { }
+
+        @Override
+        public void onViewAttachedToWindow(View view) {
+            view.getViewTreeObserver().addOnDrawListener(new MyViewDrawListener(view.getHandler()));
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View view) { }
+
+        private class MyViewDrawListener implements OnDrawListener, Runnable {
+
+            private final Handler mHandler;
+
+            MyViewDrawListener(Handler handler) {
+                mHandler = handler;
+            }
+
+            @Override
+            public void onDraw() {
+                mCurrentThreadDrawing.set(true);
+                Utilities.postAsyncCallback(mHandler, this);
+            }
+
+            @Override
+            public void run() {
+                mCurrentThreadDrawing.set(false);
+            }
+        }
+
+        private void onBitmapCreated() {
+            if (mCurrentThreadDrawing.get()) {
+                Log.e(TAG, "Bitmap created during draw pass", new Exception());
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index f579451..21822a3 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -30,14 +30,13 @@
 import android.view.View;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
 
 import java.nio.ByteBuffer;
 
@@ -53,7 +52,7 @@
     // The padding added to the drag view during the preview generation.
     public final int previewPadding;
 
-    protected final int blurSizeOutline;
+    public final int blurSizeOutline;
 
     private OutlineGeneratorCallback mOutlineGeneratorCallback;
     public Bitmap generatedDragOutline;
@@ -66,56 +65,26 @@
         mView = view;
         blurSizeOutline =
                 context.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
-
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            previewPadding = blurSizeOutline - bounds.left - bounds.top;
-        } else {
-            previewPadding = blurSizeOutline;
-        }
+        previewPadding = blurSizeOutline;
     }
 
     /**
      * Draws the {@link #mView} into the given {@param destCanvas}.
      */
     protected void drawDragView(Canvas destCanvas, float scale) {
-        destCanvas.save();
+        int saveCount = destCanvas.save();
         destCanvas.scale(scale, scale);
 
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            destCanvas.translate(blurSizeOutline / 2 - bounds.left,
-                    blurSizeOutline / 2 - bounds.top);
-            if (d instanceof FastBitmapDrawable) {
-                ((FastBitmapDrawable) d).setScale(1);
-            }
-            d.draw(destCanvas);
-        } else {
-            final Rect clipRect = mTempRect;
-            mView.getDrawingRect(clipRect);
-
-            boolean textVisible = false;
-            if (mView instanceof FolderIcon) {
-                // For FolderIcons the text can bleed into the icon area, and so we need to
-                // hide the text completely (which can't be achieved by clipping).
-                if (((FolderIcon) mView).getTextVisible()) {
-                    ((FolderIcon) mView).setTextVisible(false);
-                    textVisible = true;
-                }
-            }
-            destCanvas.translate(-mView.getScrollX() + blurSizeOutline / 2,
-                    -mView.getScrollY() + blurSizeOutline / 2);
-            destCanvas.clipRect(clipRect);
-            mView.draw(destCanvas);
-
-            // Restore text visibility of FolderIcon if necessary
-            if (textVisible) {
-                ((FolderIcon) mView).setTextVisible(true);
+        if (mView instanceof DraggableView) {
+            DraggableView dv = (DraggableView) mView;
+            try (SafeCloseable t = dv.prepareDrawDragView()) {
+                dv.getSourceVisualDragBounds(mTempRect);
+                destCanvas.translate(blurSizeOutline / 2 - mTempRect.left,
+                        blurSizeOutline / 2 - mTempRect.top);
+                mView.draw(destCanvas);
             }
         }
-        destCanvas.restore();
+        destCanvas.restoreToCount(saveCount);
     }
 
     /**
@@ -123,36 +92,25 @@
      * Responsibility for the bitmap is transferred to the caller.
      */
     public Bitmap createDragBitmap() {
-        int width = mView.getWidth();
-        int height = mView.getHeight();
-
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            width = bounds.width();
-            height = bounds.height();
-        } else if (mView instanceof LauncherAppWidgetHostView) {
-            float scale = ((LauncherAppWidgetHostView) mView).getScaleToFit();
-            width = (int) (mView.getWidth() * scale);
-            height = (int) (mView.getHeight() * scale);
-
-            if (mView instanceof PendingAppWidgetHostView) {
-                // Use hardware renderer as the icon for the pending app widget may be a hw bitmap
-                return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
-                        height + blurSizeOutline, (c) -> drawDragView(c, scale));
-            } else {
-                // Use software renderer for widgets as we know that they already work
-                return BitmapRenderer.createSoftwareBitmap(width + blurSizeOutline,
-                        height + blurSizeOutline, (c) -> drawDragView(c, scale));
-            }
+        int width = 0;
+        int height = 0;
+        // Assume scaleX == scaleY, which is always the case for workspace items.
+        float scale = mView.getScaleX();
+        if (mView instanceof DraggableView) {
+            ((DraggableView) mView).getSourceVisualDragBounds(mTempRect);
+            width = mTempRect.width();
+            height = mTempRect.height();
+        } else {
+            width = mView.getWidth();
+            height = mView.getHeight();
         }
 
         return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
-                height + blurSizeOutline, (c) -> drawDragView(c, 1));
+                height + blurSizeOutline, (c) -> drawDragView(c, scale));
     }
 
     public final void generateDragOutline(Bitmap preview) {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && mOutlineGeneratorCallback != null) {
+        if (FeatureFlags.IS_STUDIO_BUILD && mOutlineGeneratorCallback != null) {
             throw new RuntimeException("Drag outline generated twice");
         }
 
diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java
deleted file mode 100644
index 837301f..0000000
--- a/src/com/android/launcher3/graphics/DrawableFactory.java
+++ /dev/null
@@ -1,120 +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.graphics;
-
-import static com.android.launcher3.graphics.IconShape.getShapePath;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Factory for creating new drawables.
- */
-public class DrawableFactory implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<DrawableFactory> INSTANCE =
-            forOverride(DrawableFactory.class, R.string.drawable_factory_class);
-
-    protected final UserHandle mMyUser = Process.myUserHandle();
-    protected final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
-        FastBitmapDrawable drawable = info.usingLowResIcon()
-                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
-                : new FastBitmapDrawable(info);
-        drawable.setIsDisabled(info.isDisabled());
-        return drawable;
-    }
-
-    public FastBitmapDrawable newIcon(Context context, BitmapInfo info, ActivityInfo target) {
-        return info.isLowRes()
-                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
-                : new FastBitmapDrawable(info);
-    }
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
-        return new PreloadIconDrawable(info, getShapePath(), context);
-    }
-
-    /**
-     * Returns a drawable that can be used as a badge for the user or null.
-     */
-    @UiThread
-    public Drawable getBadgeForUser(UserHandle user, Context context, int badgeSize) {
-        if (mMyUser.equals(user)) {
-            return null;
-        }
-
-        Bitmap badgeBitmap = getUserBadge(user, context, badgeSize);
-        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
-        d.setFilterBitmap(true);
-        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
-        return d;
-    }
-
-    protected synchronized Bitmap getUserBadge(UserHandle user, Context context, int badgeSize) {
-        Bitmap badgeBitmap = mUserBadges.get(user);
-        if (badgeBitmap != null) {
-            return badgeBitmap;
-        }
-
-        final Resources res = context.getApplicationContext().getResources();
-        badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
-
-        Drawable drawable = context.getPackageManager().getUserBadgedDrawableForDensity(
-                new BitmapDrawable(res, badgeBitmap), user, new Rect(0, 0, badgeSize, badgeSize),
-                0);
-        if (drawable instanceof BitmapDrawable) {
-            badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
-        } else {
-            badgeBitmap.eraseColor(Color.TRANSPARENT);
-            Canvas c = new Canvas(badgeBitmap);
-            drawable.setBounds(0, 0, badgeSize, badgeSize);
-            drawable.draw(c);
-            c.setBitmap(null);
-        }
-
-        mUserBadges.put(user, badgeBitmap);
-        return badgeBitmap;
-    }
-}
diff --git a/src/com/android/launcher3/graphics/GridOptionsProvider.java b/src/com/android/launcher3/graphics/GridOptionsProvider.java
index 71b4366..08d7e4c 100644
--- a/src/com/android/launcher3/graphics/GridOptionsProvider.java
+++ b/src/com/android/launcher3/graphics/GridOptionsProvider.java
@@ -1,18 +1,14 @@
 package com.android.launcher3.graphics;
 
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
 import android.content.ContentProvider;
 import android.content.ContentValues;
+import android.content.pm.PackageManager;
 import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.database.MatrixCursor;
-import android.graphics.Bitmap;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.Xml;
 
@@ -23,12 +19,10 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.Future;
 
 /**
  * Exposes various launcher grid options and allows the caller to change them.
@@ -59,21 +53,7 @@
     private static final String KEY_LIST_OPTIONS = "/list_options";
     private static final String KEY_DEFAULT_GRID = "/default_grid";
 
-    private static final String KEY_PREVIEW = "preview";
-    private static final String MIME_TYPE_PNG = "image/png";
-
-    public static final PipeDataWriter<Future<Bitmap>> BITMAP_WRITER =
-            new PipeDataWriter<Future<Bitmap>>() {
-                @Override
-                public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String s,
-                        Bundle bundle, Future<Bitmap> bitmap) {
-                    try (AutoCloseOutputStream os = new AutoCloseOutputStream(output)) {
-                        bitmap.get().compress(Bitmap.CompressFormat.PNG, 100, os);
-                    } catch (Exception e) {
-                        Log.w(TAG, "fail to write to pipe", e);
-                    }
-                }
-            };
+    private static final String METHOD_GET_PREVIEW = "get_preview";
 
     @Override
     public boolean onCreate() {
@@ -122,10 +102,6 @@
 
     @Override
     public String getType(Uri uri) {
-        List<String> segments = uri.getPathSegments();
-        if (segments.size() > 0 && KEY_PREVIEW.equals(segments.get(0))) {
-            return MIME_TYPE_PNG;
-        }
         return "vnd.android.cursor.dir/launcher_grid";
     }
 
@@ -163,29 +139,17 @@
     }
 
     @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        List<String> segments = uri.getPathSegments();
-        if (segments.size() < 2 || !KEY_PREVIEW.equals(segments.get(0))) {
-            throw new FileNotFoundException("Invalid preview url");
-        }
-        String profileName = segments.get(1);
-        if (TextUtils.isEmpty(profileName)) {
-            throw new FileNotFoundException("Invalid preview url");
+    public Bundle call(String method, String arg, Bundle extras) {
+        if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
+                Binder.getCallingPid(), Binder.getCallingUid())
+                != PackageManager.PERMISSION_GRANTED) {
+            return null;
         }
 
-        InvariantDeviceProfile idp;
-        try {
-            idp = new InvariantDeviceProfile(getContext(), profileName);
-        } catch (Exception e) {
-            throw new FileNotFoundException(e.getMessage());
+        if (!METHOD_GET_PREVIEW.equals(method)) {
+            return null;
         }
 
-        try {
-            return openPipeHelper(uri, MIME_TYPE_PNG, null,
-                    UI_HELPER_EXECUTOR.submit(new LauncherPreviewRenderer(getContext(), idp)),
-                    BITMAP_WRITER);
-        } catch (Exception e) {
-            throw new FileNotFoundException(e.getMessage());
-        }
+        return new PreviewSurfaceRenderer(getContext(), extras).render();
     }
 }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index d7b845b..885fb66 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -19,13 +19,20 @@
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 import static android.view.View.VISIBLE;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
+import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
+import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.annotation.TargetApi;
 import android.app.Fragment;
+import android.appwidget.AppWidgetHostView;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.drawable.AdaptiveIconDrawable;
@@ -48,21 +55,54 @@
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.LoaderResults;
+import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.WidgetsModel;
+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.pm.InstallSessionHelper;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.uioverrides.PredictedAppIconInflater;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -73,23 +113,109 @@
  *   4) Measure and draw the view on a canvas
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherPreviewRenderer implements Callable<Bitmap> {
+public class LauncherPreviewRenderer {
 
     private static final String TAG = "LauncherPreviewRenderer";
 
+    /**
+     * Context used just for preview. It also provides a few objects (e.g. UserCache) just for
+     * preview purposes.
+     */
+    public static class PreviewContext extends ContextWrapper {
+
+        private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
+                Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
+                        LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
+                        CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
+
+        private final InvariantDeviceProfile mIdp;
+        private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>();
+        private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
+                new ConcurrentLinkedQueue<>();
+
+        public PreviewContext(Context base, InvariantDeviceProfile idp) {
+            super(base);
+            mIdp = idp;
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return this;
+        }
+
+        public void onDestroy() {
+            CustomWidgetManager customWidgetManager = (CustomWidgetManager) mObjectMap.get(
+                    CustomWidgetManager.INSTANCE);
+            if (customWidgetManager != null) {
+                customWidgetManager.onDestroy();
+            }
+        }
+
+        /**
+         * Find a cached object from mObjectMap if we have already created one. If not, generate
+         * an object using the provider.
+         */
+        public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
+                MainThreadInitializedObject.ObjectProvider<T> provider) {
+            if (!WHITELIST.contains(mainThreadInitializedObject)) {
+                throw new IllegalStateException("Leaking unknown objects");
+            }
+            if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
+                throw new IllegalStateException(
+                        "Should not use MainThreadInitializedObject to initialize this with "
+                                + "PreviewContext");
+            }
+            if (mainThreadInitializedObject == InvariantDeviceProfile.INSTANCE) {
+                return (T) mIdp;
+            }
+            if (mObjectMap.containsKey(mainThreadInitializedObject)) {
+                return (T) mObjectMap.get(mainThreadInitializedObject);
+            }
+            T t = provider.get(this);
+            mObjectMap.put(mainThreadInitializedObject, t);
+            return t;
+        }
+
+        public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) {
+            LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
+            if (launcherIconsForPreview != null) {
+                return launcherIconsForPreview;
+            }
+            return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
+                    -1 /* poolId */, shapeDetection);
+        }
+
+        private final class LauncherIconsForPreview extends LauncherIcons {
+
+            private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
+                    int poolId, boolean shapeDetection) {
+                super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection);
+            }
+
+            @Override
+            public void recycle() {
+                // Clear any temporary state variables
+                clear();
+                mIconPool.offer(this);
+            }
+        }
+    }
+
     private final Handler mUiHandler;
     private final Context mContext;
     private final InvariantDeviceProfile mIdp;
     private final DeviceProfile mDp;
+    private final boolean mMigrated;
     private final Rect mInsets;
 
     private final WorkspaceItemInfo mWorkspaceItemInfo;
 
-    public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) {
+    public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
         mUiHandler = new Handler(Looper.getMainLooper());
         mContext = context;
         mIdp = idp;
         mDp = idp.portraitProfile.copy(context);
+        mMigrated = migrated;
 
         // TODO: get correct insets once display cutout API is available.
         mInsets = new Rect();
@@ -105,32 +231,17 @@
                 Build.VERSION.SDK_INT);
 
         mWorkspaceItemInfo = new WorkspaceItemInfo();
-        mWorkspaceItemInfo.applyFrom(iconInfo);
+        mWorkspaceItemInfo.bitmap = iconInfo;
         mWorkspaceItemInfo.intent = new Intent();
         mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
                 context.getString(R.string.label_application);
     }
 
-    @Override
-    public Bitmap call() {
-        return BitmapRenderer.createHardwareBitmap(mDp.widthPx, mDp.heightPx, c -> {
-
-            if (Looper.myLooper() == Looper.getMainLooper()) {
-                new MainThreadRenderer(mContext).renderScreenShot(c);
-            } else {
-                CountDownLatch latch = new CountDownLatch(1);
-                Utilities.postAsyncCallback(mUiHandler, () -> {
-                    new MainThreadRenderer(mContext).renderScreenShot(c);
-                    latch.countDown();
-                });
-
-                try {
-                    latch.await();
-                } catch (Exception e) {
-                    Log.e(TAG, "Error drawing on main thread", e);
-                }
-            }
-        });
+    /** Populate preview and render it. */
+    public View getRenderedView() {
+        MainThreadRenderer renderer = new MainThreadRenderer(mContext);
+        renderer.populate();
+        return renderer.mRootView;
     }
 
     private class MainThreadRenderer extends ContextThemeWrapper
@@ -222,6 +333,32 @@
             addInScreenFromBind(icon, info);
         }
 
+        private void inflateAndAddFolder(FolderInfo info) {
+            FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+                    info);
+            addInScreenFromBind(folderIcon, info);
+        }
+
+        private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
+            WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
+                    info.providerName);
+            if (widgetItem == null) {
+                return;
+            }
+            AppWidgetHostView view = new AppWidgetHostView(mContext);
+            view.setAppWidget(-1, widgetItem.widgetInfo);
+            view.updateAppWidget(null);
+            view.setTag(info);
+            addInScreenFromBind(view, info);
+        }
+
+        private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
+            View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
+            if (view != null) {
+                addInScreenFromBind(view, info);
+            }
+        }
+
         private void dispatchVisibilityAggregated(View view, boolean isVisible) {
             // Similar to View.dispatchVisibilityAggregated implementation.
             final boolean thisVisible = view.getVisibility() == VISIBLE;
@@ -240,23 +377,101 @@
             }
         }
 
-        private void renderScreenShot(Canvas canvas) {
-            // Add hotseat icons
-            for (int i = 0; i < mIdp.numHotseatIcons; i++) {
-                WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
-                info.container = Favorites.CONTAINER_HOTSEAT;
-                info.screenId = i;
-                inflateAndAddIcon(info);
-            }
+        private void populate() {
+            if (ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get()) {
+                WorkspaceFetcher fetcher;
+                PreviewContext previewContext = null;
+                if (mMigrated) {
+                    previewContext = new PreviewContext(mContext, mIdp);
+                    LauncherAppState appForPreview = new LauncherAppState(
+                            previewContext, null /* iconCacheFileName */);
+                    fetcher = new WorkspaceItemsInfoFromPreviewFetcher(appForPreview);
+                    MODEL_EXECUTOR.execute(fetcher);
+                } else {
+                    fetcher = new WorkspaceItemsInfoFetcher();
+                    LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
+                            (LauncherModel.ModelUpdateTask) fetcher);
+                }
+                WorkspaceResult workspaceResult = fetcher.get();
+                if (previewContext != null) {
+                    previewContext.onDestroy();
+                }
 
-            // Add workspace icons
-            for (int i = 0; i < mIdp.numColumns; i++) {
-                WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
-                info.container = Favorites.CONTAINER_DESKTOP;
-                info.screenId = 0;
-                info.cellX = i;
-                info.cellY = mIdp.numRows - 1;
-                inflateAndAddIcon(info);
+                if (workspaceResult == null) {
+                    return;
+                }
+
+                // Separate the items that are on the current screen, and all the other remaining
+                // items
+                ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+                ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+                ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+                ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+                filterCurrentWorkspaceItems(0 /* currentScreenId */,
+                        workspaceResult.mWorkspaceItems, currentWorkspaceItems,
+                        otherWorkspaceItems);
+                filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceResult.mAppWidgets,
+                        currentAppWidgets, otherAppWidgets);
+                sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
+
+                for (ItemInfo itemInfo : currentWorkspaceItems) {
+                    switch (itemInfo.itemType) {
+                        case Favorites.ITEM_TYPE_APPLICATION:
+                        case Favorites.ITEM_TYPE_SHORTCUT:
+                        case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                            inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                            inflateAndAddFolder((FolderInfo) itemInfo);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+                for (ItemInfo itemInfo : currentAppWidgets) {
+                    switch (itemInfo.itemType) {
+                        case Favorites.ITEM_TYPE_APPWIDGET:
+                        case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+                            inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
+                                    workspaceResult.mWidgetsModel);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+
+                IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
+                        mIdp.numHotseatIcons);
+                int count = Math.min(ranks.size(), workspaceResult.mCachedPredictedItems.size());
+                for (int i = 0; i < count; i++) {
+                    AppInfo appInfo = workspaceResult.mCachedPredictedItems.get(i);
+                    int rank = ranks.get(i);
+                    WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(appInfo);
+                    itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+                    itemInfo.rank = rank;
+                    itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+                    itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
+                    itemInfo.screenId = rank;
+                    inflateAndAddPredictedIcon(itemInfo);
+                }
+            } else {
+                // Add hotseat icons
+                for (int i = 0; i < mIdp.numHotseatIcons; i++) {
+                    WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
+                    info.container = Favorites.CONTAINER_HOTSEAT;
+                    info.screenId = i;
+                    inflateAndAddIcon(info);
+                }
+                // Add workspace icons
+                for (int i = 0; i < mIdp.numColumns; i++) {
+                    WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
+                    info.container = Favorites.CONTAINER_DESKTOP;
+                    info.screenId = 0;
+                    info.cellX = i;
+                    info.cellY = mIdp.numRows - 1;
+                    inflateAndAddIcon(info);
+                }
             }
 
             // Add first page QSB
@@ -280,9 +495,6 @@
             measureView(mRootView, mDp.widthPx, mDp.heightPx);
             // Additional measure for views which use auto text size API
             measureView(mRootView, mDp.widthPx, mDp.heightPx);
-
-            mRootView.draw(canvas);
-            dispatchVisibilityAggregated(mRootView, false);
         }
     }
 
@@ -290,4 +502,105 @@
         view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
         view.layout(0, 0, width, height);
     }
+
+    private static class WorkspaceItemsInfoFetcher implements LauncherModel.ModelUpdateTask,
+            WorkspaceFetcher {
+
+        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
+
+        private LauncherAppState mApp;
+        private LauncherModel mModel;
+        private BgDataModel mBgDataModel;
+        private AllAppsList mAllAppsList;
+
+        @Override
+        public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
+                AllAppsList allAppsList, Executor uiExecutor) {
+            mApp = app;
+            mModel = model;
+            mBgDataModel = dataModel;
+            mAllAppsList = allAppsList;
+        }
+
+        @Override
+        public FutureTask<WorkspaceResult> getTask() {
+            return mTask;
+        }
+
+        @Override
+        public void run() {
+            mTask.run();
+        }
+
+        @Override
+        public WorkspaceResult call() throws Exception {
+            if (!mModel.isModelLoaded()) {
+                Log.d(TAG, "Workspace not loaded, loading now");
+                mModel.startLoaderForResults(
+                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
+                return null;
+            }
+
+            return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
+                    mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
+        }
+    }
+
+    private static class WorkspaceItemsInfoFromPreviewFetcher extends LoaderTask implements
+            WorkspaceFetcher {
+
+        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
+
+        WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
+            super(app, null, new BgDataModel(), null);
+        }
+
+        @Override
+        public FutureTask<WorkspaceResult> getTask() {
+            return mTask;
+        }
+
+        @Override
+        public void run() {
+            mTask.run();
+        }
+
+        @Override
+        public WorkspaceResult call() throws Exception {
+            List<ShortcutInfo> allShortcuts = new ArrayList<>();
+            loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI);
+            mBgDataModel.widgetsModel.update(mApp, null);
+            return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
+                    mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
+        }
+    }
+
+    private interface WorkspaceFetcher extends Runnable, Callable<WorkspaceResult> {
+        FutureTask<WorkspaceResult> getTask();
+
+        default WorkspaceResult get() {
+            try {
+                return getTask().get(5, TimeUnit.SECONDS);
+            } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                Log.d(TAG, "Error fetching workspace items info", e);
+                return null;
+            }
+        }
+    }
+
+    private static class WorkspaceResult {
+        private final ArrayList<ItemInfo> mWorkspaceItems;
+        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final ArrayList<AppInfo> mCachedPredictedItems;
+        private final WidgetsModel mWidgetsModel;
+
+        private WorkspaceResult(ArrayList<ItemInfo> workspaceItems,
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                ArrayList<AppInfo> cachedPredictedItems, WidgetsModel widgetsModel) {
+            mWorkspaceItems = workspaceItems;
+            mAppWidgets = appWidgets;
+            mCachedPredictedItems = cachedPredictedItems;
+            mWidgetsModel = widgetsModel;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java
index d707403..94acbfd 100644
--- a/src/com/android/launcher3/graphics/OverviewScrim.java
+++ b/src/com/android/launcher3/graphics/OverviewScrim.java
@@ -55,16 +55,17 @@
         mCurrentScrimmedView = mStableScrimmedView;
         int currentIndex = root.indexOfChild(mCurrentScrimmedView);
         final int childCount = root.getChildCount();
-        while (mCurrentScrimmedView.getVisibility() != VISIBLE && currentIndex < childCount) {
+        while (mCurrentScrimmedView != null && mCurrentScrimmedView.getVisibility() != VISIBLE
+                && currentIndex < childCount) {
             currentIndex++;
             mCurrentScrimmedView = root.getChildAt(currentIndex);
         }
     }
 
     /**
-     * @return The view to draw the scrim behind.
+     * @return The view to draw the scrim behind, or null if all visible views should be scrimmed.
      */
-    public View getScrimmedView() {
+    public @Nullable View getScrimmedView() {
         return mCurrentScrimmedView;
     }
 }
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
index 23745cb..d347e8f 100644
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
@@ -17,14 +17,14 @@
 
 import static androidx.core.graphics.ColorUtils.compositeColors;
 
+import static com.android.launcher3.graphics.IconShape.getShapePath;
+
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
 
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.util.Themes;
@@ -37,20 +37,12 @@
     // Path in [0, 100] bounds.
     private final Path mProgressPath;
 
-    public PlaceHolderIconDrawable(BitmapInfo info, Path progressPath, Context context) {
-        this(info.icon, info.color, progressPath, context);
-    }
+    public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
+        super(info);
 
-    public PlaceHolderIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
-        this(info.iconBitmap, info.iconColor, progressPath, context);
-    }
-
-    protected PlaceHolderIconDrawable(Bitmap b, int iconColor, Path progressPath, Context context) {
-        super(b, iconColor);
-
-        mProgressPath = progressPath;
+        mProgressPath = getShapePath();
         mPaint.setColor(compositeColors(
-                Themes.getAttrColor(context, R.attr.loadingIconColor), iconColor));
+                Themes.getAttrColor(context, R.attr.loadingIconColor), info.color));
     }
 
     @Override
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index cc4c2ef..e85b056 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -18,6 +18,7 @@
 package com.android.launcher3.graphics;
 
 import static com.android.launcher3.graphics.IconShape.DEFAULT_PATH_SIZE;
+import static com.android.launcher3.graphics.IconShape.getShapePath;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -30,12 +31,13 @@
 import android.graphics.Path;
 import android.graphics.PathMeasure;
 import android.graphics.Rect;
+import android.util.Pair;
 import android.util.Property;
 import android.util.SparseArray;
 
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 
 import java.lang.ref.WeakReference;
 
@@ -72,7 +74,8 @@
 
     private static final float SMALL_SCALE = 0.6f;
 
-    private static final SparseArray<WeakReference<Bitmap>> sShadowCache = new SparseArray<>();
+    private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache =
+            new SparseArray<>();
 
     private final Matrix mTmpMatrix = new Matrix();
     private final PathMeasure mPathMeasure = new PathMeasure();
@@ -80,7 +83,7 @@
     private final ItemInfoWithIcon mItem;
 
     // Path in [0, 100] bounds.
-    private final Path mProgressPath;
+    private final Path mShapePath;
 
     private final Path mScaledTrackPath;
     private final Path mScaledProgressPath;
@@ -101,13 +104,10 @@
 
     private ObjectAnimator mCurrentAnim;
 
-    /**
-     * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar.
-     */
-    public PreloadIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
-        super(info);
+    public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
+        super(info.bitmap);
         mItem = info;
-        mProgressPath = progressPath;
+        mShapePath = getShapePath();
         mScaledTrackPath = new Path();
         mScaledProgressPath = new Path();
 
@@ -129,7 +129,7 @@
                 bounds.left + PROGRESS_WIDTH + PROGRESS_GAP,
                 bounds.top + PROGRESS_WIDTH + PROGRESS_GAP);
 
-        mProgressPath.transform(mTmpMatrix, mScaledTrackPath);
+        mShapePath.transform(mTmpMatrix, mScaledTrackPath);
         float scale = bounds.width() / DEFAULT_PATH_SIZE;
         mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale);
 
@@ -143,8 +143,9 @@
 
     private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
         int key = (width << 16) | height;
-        WeakReference<Bitmap> shadowRef = sShadowCache.get(key);
-        Bitmap shadow = shadowRef != null ? shadowRef.get() : null;
+        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;
         if (shadow != null) {
             return shadow;
         }
@@ -157,7 +158,7 @@
         mProgressPaint.clearShadowLayer();
         c.setBitmap(null);
 
-        sShadowCache.put(key, new WeakReference<>(shadow));
+        sShadowCache.put(key, new WeakReference<>(Pair.create(mShapePath, shadow)));
         return shadow;
     }
 
@@ -289,4 +290,11 @@
         }
         invalidateSelf();
     }
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
+        return new PreloadIconDrawable(info, context);
+    }
 }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
new file mode 100644
index 0000000..fdc3a94
--- /dev/null
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -0,0 +1,168 @@
+/*
+ * 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.graphics;
+
+import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.model.GridSizeMigrationTask;
+import com.android.launcher3.model.GridSizeMigrationTaskV2;
+
+import java.util.concurrent.TimeUnit;
+
+/** Render preview using surface view. */
+public class PreviewSurfaceRenderer implements IBinder.DeathRecipient {
+
+    private static final int FADE_IN_ANIMATION_DURATION = 200;
+
+    private static final String KEY_HOST_TOKEN = "host_token";
+    private static final String KEY_VIEW_WIDTH = "width";
+    private static final String KEY_VIEW_HEIGHT = "height";
+    private static final String KEY_DISPLAY_ID = "display_id";
+    private static final String KEY_SURFACE_PACKAGE = "surface_package";
+    private static final String KEY_CALLBACK = "callback";
+
+    private final Context mContext;
+    private final InvariantDeviceProfile mIdp;
+    private final IBinder mHostToken;
+    private final int mWidth;
+    private final int mHeight;
+    private final Display mDisplay;
+
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+
+    PreviewSurfaceRenderer(Context context, Bundle bundle) {
+        mContext = context;
+
+        String gridName = bundle.getString("name");
+        bundle.remove("name");
+        if (gridName == null) {
+            gridName = InvariantDeviceProfile.getCurrentGridName(context);
+        }
+        mIdp = new InvariantDeviceProfile(context, gridName);
+
+        mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
+        mWidth = bundle.getInt(KEY_VIEW_WIDTH);
+        mHeight = bundle.getInt(KEY_VIEW_HEIGHT);
+
+        final DisplayManager displayManager = (DisplayManager) context.getSystemService(
+                Context.DISPLAY_SERVICE);
+        mDisplay = displayManager.getDisplay(bundle.getInt(KEY_DISPLAY_ID));
+    }
+
+    /** Handle a received surface view request. */
+    Bundle render() {
+        if (mSurfaceControlViewHost != null) {
+            binderDied();
+        }
+
+        SurfaceControlViewHost.SurfacePackage surfacePackage;
+        try {
+            mSurfaceControlViewHost = MAIN_EXECUTOR
+                    .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
+                    .get(5, TimeUnit.SECONDS);
+            surfacePackage = mSurfaceControlViewHost.getSurfacePackage();
+            mHostToken.linkToDeath(this, 0);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+
+        MODEL_EXECUTOR.post(() -> {
+            final boolean success = doGridMigrationIfNecessary();
+
+            MAIN_EXECUTOR.post(() -> {
+                // If mSurfaceControlViewHost is null due to any reason (e.g. binder died,
+                // happening when user leaves the preview screen before preview rendering finishes),
+                // we should return here.
+                SurfaceControlViewHost host = mSurfaceControlViewHost;
+                if (host == null) {
+                    return;
+                }
+
+                View view = new LauncherPreviewRenderer(mContext, mIdp, success).getRenderedView();
+                // This aspect scales the view to fit in the surface and centers it
+                final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
+                        mHeight / (float) view.getMeasuredHeight());
+                view.setScaleX(scale);
+                view.setScaleY(scale);
+                view.setPivotX(0);
+                view.setPivotY(0);
+                view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
+                view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+                view.setAlpha(0);
+                view.animate().alpha(1)
+                        .setInterpolator(new AccelerateDecelerateInterpolator())
+                        .setDuration(FADE_IN_ANIMATION_DURATION)
+                        .start();
+                host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
+            });
+        });
+
+        Bundle result = new Bundle();
+        result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);
+
+        Handler handler = new Handler(Looper.getMainLooper(), message -> {
+            binderDied();
+            return true;
+        });
+        Messenger messenger = new Messenger(handler);
+        Message msg = Message.obtain();
+        msg.replyTo = messenger;
+        result.putParcelable(KEY_CALLBACK, msg);
+        return result;
+    }
+
+    @Override
+    public void binderDied() {
+        if (mSurfaceControlViewHost != null) {
+            MAIN_EXECUTOR.execute(() -> {
+                mSurfaceControlViewHost.release();
+                mSurfaceControlViewHost = null;
+            });
+        }
+        mHostToken.unlinkToDeath(this, 0);
+    }
+
+    private boolean doGridMigrationIfNecessary() {
+        boolean needsToMigrate =
+                MULTI_DB_GRID_MIRATION_ALGO.get()
+                        ? GridSizeMigrationTaskV2.needsToMigrate(mContext, mIdp)
+                        : GridSizeMigrationTask.needsToMigrate(mContext, mIdp);
+        if (!needsToMigrate) {
+            return false;
+        }
+        return MULTI_DB_GRID_MIRATION_ALGO.get()
+                ? GridSizeMigrationTaskV2.migrateGridIfNeeded(mContext, mIdp)
+                : GridSizeMigrationTask.migrateGridIfNeeded(mContext, mIdp);
+    }
+}
diff --git a/src/com/android/launcher3/graphics/RotationMode.java b/src/com/android/launcher3/graphics/RotationMode.java
deleted file mode 100644
index b06305f..0000000
--- a/src/com/android/launcher3/graphics/RotationMode.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.graphics;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-public abstract class RotationMode {
-
-    public static RotationMode NORMAL = new RotationMode(0) { };
-
-    public final float surfaceRotation;
-    public final boolean isTransposed;
-
-    public RotationMode(float surfaceRotation) {
-        this.surfaceRotation = surfaceRotation;
-        isTransposed = surfaceRotation != 0;
-    }
-
-    public final void mapRect(Rect rect, Rect out) {
-        mapRect(rect.left, rect.top, rect.right, rect.bottom, out);
-    }
-
-    public void mapRect(int left, int top, int right, int bottom, Rect out) {
-        out.set(left, top, right, bottom);
-    }
-
-    public void mapInsets(Context context, Rect insets, Rect out) {
-        out.set(insets);
-    }
-
-    public int toNaturalGravity(int absoluteGravity) {
-        return absoluteGravity;
-    }
-}
diff --git a/src/com/android/launcher3/graphics/Scrim.java b/src/com/android/launcher3/graphics/Scrim.java
index 5c14f8d..f90962d 100644
--- a/src/com/android/launcher3/graphics/Scrim.java
+++ b/src/com/android/launcher3/graphics/Scrim.java
@@ -19,7 +19,7 @@
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.graphics.Canvas;
-import android.util.Property;
+import android.util.FloatProperty;
 import android.view.View;
 
 import com.android.launcher3.Launcher;
@@ -31,16 +31,16 @@
 public class Scrim implements View.OnAttachStateChangeListener,
         WallpaperColorInfo.OnChangeListener {
 
-    public static Property<Scrim, Float> SCRIM_PROGRESS =
-            new Property<Scrim, Float>(Float.TYPE, "scrimProgress") {
+    public static final FloatProperty<Scrim> SCRIM_PROGRESS =
+            new FloatProperty<Scrim>("scrimProgress") {
                 @Override
                 public Float get(Scrim scrim) {
                     return scrim.mScrimProgress;
                 }
 
                 @Override
-                public void set(Scrim scrim, Float value) {
-                    scrim.setScrimProgress(value);
+                public void setValue(Scrim scrim, float v) {
+                    scrim.setScrimProgress(v);
                 }
             };
 
@@ -55,7 +55,7 @@
     public Scrim(View view) {
         mRoot = view;
         mLauncher = Launcher.getLauncher(view.getContext());
-        mWallpaperColorInfo = WallpaperColorInfo.getInstance(mLauncher);
+        mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(mLauncher);
 
         view.addOnAttachStateChangeListener(this);
     }
diff --git a/src/com/android/launcher3/graphics/ShadowDrawable.java b/src/com/android/launcher3/graphics/ShadowDrawable.java
index f10b972..d8a7070 100644
--- a/src/com/android/launcher3/graphics/ShadowDrawable.java
+++ b/src/com/android/launcher3/graphics/ShadowDrawable.java
@@ -120,36 +120,35 @@
     }
 
     private void regenerateBitmapCache() {
-        Bitmap bitmap = Bitmap.createBitmap(mState.mIntrinsicWidth, mState.mIntrinsicHeight,
-                Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-
         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
         Drawable d = mState.mChildState.newDrawable().mutate();
         d.setBounds(mState.mShadowSize, mState.mShadowSize,
                 mState.mIntrinsicWidth - mState.mShadowSize,
                 mState.mIntrinsicHeight - mState.mShadowSize);
         d.setTint(mState.mIsDark ? mState.mDarkTintColor : Color.WHITE);
-        d.draw(canvas);
 
-        // Do not draw shadow on dark theme
-        if (!mState.mIsDark) {
+        if (mState.mIsDark) {
+            // Dark text do not have any shadow, but just the bitmap
+            mState.mLastDrawnBitmap = BitmapRenderer.createHardwareBitmap(
+                    mState.mIntrinsicWidth, mState.mIntrinsicHeight, d::draw);
+        } else {
             Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
             paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, BlurMaskFilter.Blur.NORMAL));
+
+            // Generate the shadow bitmap
             int[] offset = new int[2];
-            Bitmap shadow = bitmap.extractAlpha(paint, offset);
+            Bitmap shadow = BitmapRenderer.createSoftwareBitmap(
+                    mState.mIntrinsicWidth, mState.mIntrinsicHeight, d::draw)
+                    .extractAlpha(paint, offset);
 
             paint.setMaskFilter(null);
             paint.setColor(mState.mShadowColor);
-            bitmap.eraseColor(Color.TRANSPARENT);
-            canvas.drawBitmap(shadow, offset[0], offset[1], paint);
-            d.draw(canvas);
+            mState.mLastDrawnBitmap = BitmapRenderer.createHardwareBitmap(
+                    mState.mIntrinsicWidth, mState.mIntrinsicHeight, c -> {
+                        c.drawBitmap(shadow, offset[0], offset[1], paint);
+                        d.draw(c);
+                    });
         }
-
-        if (BitmapRenderer.USE_HARDWARE_BITMAP) {
-            bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
-        }
-        mState.mLastDrawnBitmap = bitmap;
     }
 
     @Override
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
index 15ff207..7b7ab5e 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
@@ -19,6 +19,7 @@
 import static android.content.Intent.ACTION_SCREEN_OFF;
 import static android.content.Intent.ACTION_USER_PRESENT;
 
+import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.ObjectAnimator;
@@ -37,14 +38,16 @@
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.util.DisplayMetrics;
-import android.util.Property;
+import android.util.FloatProperty;
 import android.view.View;
+import android.view.WindowInsets;
 
 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;
@@ -54,33 +57,37 @@
  */
 public class WorkspaceAndHotseatScrim extends Scrim {
 
-    public static Property<WorkspaceAndHotseatScrim, Float> SYSUI_PROGRESS =
-            new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "sysUiProgress") {
+    public static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_PROGRESS =
+            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiProgress") {
                 @Override
                 public Float get(WorkspaceAndHotseatScrim scrim) {
                     return scrim.mSysUiProgress;
                 }
 
                 @Override
-                public void set(WorkspaceAndHotseatScrim scrim, Float value) {
+                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
                     scrim.setSysUiProgress(value);
                 }
             };
 
-    private static Property<WorkspaceAndHotseatScrim, Float> SYSUI_ANIM_MULTIPLIER =
-            new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "sysUiAnimMultiplier") {
+    private static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_ANIM_MULTIPLIER =
+            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiAnimMultiplier") {
                 @Override
                 public Float get(WorkspaceAndHotseatScrim scrim) {
                     return scrim.mSysUiAnimMultiplier;
                 }
 
                 @Override
-                public void set(WorkspaceAndHotseatScrim scrim, Float value) {
+                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
                     scrim.mSysUiAnimMultiplier = value;
                     scrim.reapplySysUiAlpha();
                 }
             };
 
+    /**
+     * Receiver used to get a signal that the user unlocked their device.
+     * @see KEYGUARD_ANIMATION For proper signal.
+     */
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -164,8 +171,10 @@
                 mSysUiAnimMultiplier = 0;
                 reapplySysUiAlphaNoInvalidate();
 
-                animateToSysuiMultiplier(1, 600,
-                        mLauncher.getWindow().getTransitionBackgroundFadeDuration());
+                ObjectAnimator oa = createSysuiMultiplierAnim(1);
+                oa.setDuration(600);
+                oa.setStartDelay(mLauncher.getWindow().getTransitionBackgroundFadeDuration());
+                oa.start();
                 mAnimateScrimOnNextDraw = false;
             }
 
@@ -178,29 +187,42 @@
         }
     }
 
-    public void animateToSysuiMultiplier(float toMultiplier, long duration,
-            long startDelay) {
-        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, toMultiplier);
+    /**
+     * @return an ObjectAnimator that controls the fade in/out of the sys ui scrim.
+     */
+    public ObjectAnimator createSysuiMultiplierAnim(float... values) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, values);
         anim.setAutoCancel(true);
-        anim.setDuration(duration);
-        anim.setStartDelay(startDelay);
-        anim.start();
+        return anim;
     }
 
     /**
      * Determines whether to draw the top and/or bottom scrim based on new insets.
      */
     public void onInsetsChanged(Rect insets, boolean allowSysuiScrims) {
-        mDrawTopScrim = allowSysuiScrims && mTopScrim != null && insets.top > 0;
-        mDrawBottomScrim = allowSysuiScrims && mBottomMask != null
-                && !mLauncher.getDeviceProfile().isVerticalBarLayout();
+        mDrawTopScrim = allowSysuiScrims
+                && mTopScrim != null
+                && insets.top > 0;
+        mDrawBottomScrim = allowSysuiScrims
+                && mBottomMask != null
+                && !mLauncher.getDeviceProfile().isVerticalBarLayout()
+                && hasBottomNavButtons();
+    }
+
+    private boolean hasBottomNavButtons() {
+        if (Utilities.ATLEAST_Q && mLauncher.getRootView() != null
+                && mLauncher.getRootView().getRootWindowInsets() != null) {
+            WindowInsets windowInsets = mLauncher.getRootView().getRootWindowInsets();
+            return windowInsets.getTappableElementInsets().bottom > 0;
+        }
+        return true;
     }
 
     @Override
     public void onViewAttachedToWindow(View view) {
         super.onViewAttachedToWindow(view);
 
-        if (mTopScrim != null) {
+        if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
             IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
             filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
             mRoot.getContext().registerReceiver(mReceiver, filter);
@@ -210,7 +232,7 @@
     @Override
     public void onViewDetachedFromWindow(View view) {
         super.onViewDetachedFromWindow(view);
-        if (mTopScrim != null) {
+        if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
             mRoot.getContext().unregisterReceiver(mReceiver);
         }
     }
@@ -232,14 +254,6 @@
         }
     }
 
-    public void hideSysUiScrim(boolean hideSysUiScrim) {
-        mHideSysUiScrim = hideSysUiScrim || (mTopScrim == null);
-        if (!hideSysUiScrim) {
-            mAnimateScrimOnNextDraw = true;
-        }
-        invalidate();
-    }
-
     private void setSysUiProgress(float progress) {
         if (progress != mSysUiProgress) {
             mSysUiProgress = progress;
@@ -262,7 +276,7 @@
         }
     }
 
-    public Bitmap createDitheredAlphaMask() {
+    private Bitmap createDitheredAlphaMask() {
         DisplayMetrics dm = mLauncher.getResources().getDisplayMetrics();
         int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
         int gradientHeight = ResourceUtils.pxFromDp(ALPHA_MASK_HEIGHT_DP, dm);
diff --git a/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
new file mode 100644
index 0000000..b7dd092
--- /dev/null
+++ b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.icons;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.launcher3.FastBitmapDrawable;
+
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
+ * clock icons
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
+
+    private static final String TAG = "ClockDrawableWrapper";
+
+    private static final boolean DISABLE_SECONDS = true;
+
+    // Time after which the clock icon should check for an update. The actual invalidate
+    // will only happen in case of any change.
+    public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
+
+    private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
+    private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".LEVEL_PER_TICK_ICON_ROUND";
+    private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
+    private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".MINUTE_LAYER_INDEX";
+    private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".SECOND_LAYER_INDEX";
+    private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_HOUR";
+    private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_MINUTE";
+    private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_SECOND";
+
+    /* Number of levels to jump per second for the second hand */
+    private static final int LEVELS_PER_SECOND = 10;
+
+    public static final int INVALID_VALUE = -1;
+
+    private final AnimationInfo mAnimationInfo = new AnimationInfo();
+    private int mTargetSdkVersion;
+
+    public ClockDrawableWrapper(AdaptiveIconDrawable base) {
+        super(base.getBackground(), base.getForeground());
+    }
+
+    /**
+     * Loads and returns the wrapper from the provided package, or returns null
+     * if it is unable to load.
+     */
+    public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
+        try {
+            PackageManager pm = context.getPackageManager();
+            ApplicationInfo appInfo =  pm.getApplicationInfo(pkg,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
+            final Bundle metadata = appInfo.metaData;
+            if (metadata == null) {
+                return null;
+            }
+            int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
+            if (drawableId == 0) {
+                return null;
+            }
+
+            Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
+                    drawableId, iconDpi).mutate();
+            if (!(drawable instanceof AdaptiveIconDrawable)) {
+                return null;
+            }
+
+            ClockDrawableWrapper wrapper =
+                    new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
+            wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
+            AnimationInfo info = wrapper.mAnimationInfo;
+
+            info.baseDrawableState = drawable.getConstantState();
+
+            info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
+            info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
+            info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
+
+            info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
+            info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
+            info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
+
+            LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
+            int layerCount = foreground.getNumberOfLayers();
+            if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
+                info.hourLayerIndex = INVALID_VALUE;
+            }
+            if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
+                info.minuteLayerIndex = INVALID_VALUE;
+            }
+            if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
+                info.secondLayerIndex = INVALID_VALUE;
+            } else if (DISABLE_SECONDS) {
+                foreground.setDrawable(info.secondLayerIndex, null);
+                info.secondLayerIndex = INVALID_VALUE;
+            }
+            return wrapper;
+        } catch (Exception e) {
+            Log.d(TAG, "Unable to load clock drawable info", e);
+        }
+        return null;
+    }
+
+    @Override
+    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+        iconFactory.disableColorExtraction();
+        float [] scale = new float[1];
+        AdaptiveIconDrawable background = new AdaptiveIconDrawable(
+                getBackground().getConstantState().newDrawable(), null);
+        BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
+                Process.myUserHandle(), mTargetSdkVersion, false, scale);
+
+        return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
+    }
+
+    @Override
+    public void prepareToDrawOnUi() {
+        mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
+    }
+
+    private static class AnimationInfo {
+
+        public ConstantState baseDrawableState;
+
+        public int hourLayerIndex;
+        public int minuteLayerIndex;
+        public int secondLayerIndex;
+        public int defaultHour;
+        public int defaultMinute;
+        public int defaultSecond;
+
+        boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
+            time.setTimeInMillis(System.currentTimeMillis());
+
+            // We need to rotate by the difference from the default time if one is specified.
+            int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
+            int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
+            int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
+
+            boolean invalidate = false;
+            if (hourLayerIndex != INVALID_VALUE) {
+                final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
+                if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
+                    invalidate = true;
+                }
+            }
+
+            if (minuteLayerIndex != INVALID_VALUE) {
+                final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
+                if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
+                    invalidate = true;
+                }
+            }
+
+            if (secondLayerIndex != INVALID_VALUE) {
+                final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
+                if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
+                    invalidate = true;
+                }
+            }
+
+            return invalidate;
+        }
+    }
+
+    private static class ClockBitmapInfo extends BitmapInfo implements FastBitmapDrawable.Factory {
+
+        public final float scale;
+        public final int offset;
+        public final AnimationInfo animInfo;
+        public final Bitmap mFlattenedBackground;
+
+        ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
+                Bitmap background) {
+            super(icon, color);
+            this.scale = scale;
+            this.animInfo = animInfo;
+            this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
+            this.mFlattenedBackground = background;
+        }
+
+        @Override
+        public FastBitmapDrawable newDrawable() {
+            return new ClockIconDrawable(this);
+        }
+    }
+
+    private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
+
+        private final Calendar mTime = Calendar.getInstance();
+
+        private final ClockBitmapInfo mInfo;
+
+        private final AdaptiveIconDrawable mFullDrawable;
+        private final LayerDrawable mForeground;
+
+        ClockIconDrawable(ClockBitmapInfo clockInfo) {
+            super(clockInfo);
+
+            mInfo = clockInfo;
+
+            mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState.newDrawable();
+            mForeground = (LayerDrawable) mFullDrawable.getForeground();
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            super.onBoundsChange(bounds);
+            mFullDrawable.setBounds(bounds);
+        }
+
+        @Override
+        public void drawInternal(Canvas canvas, Rect bounds) {
+            if (mInfo == null) {
+                super.drawInternal(canvas, bounds);
+                return;
+            }
+            // draw the background that is already flattened to a bitmap
+            canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
+
+            // prepare and draw the foreground
+            mInfo.animInfo.applyTime(mTime, mForeground);
+
+            canvas.scale(mInfo.scale, mInfo.scale,
+                    bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
+            canvas.clipPath(mFullDrawable.getIconMask());
+            mForeground.draw(canvas);
+
+            reschedule();
+        }
+
+        @Override
+        protected void updateFilter() {
+            super.updateFilter();
+            mFullDrawable.setColorFilter(mPaint.getColorFilter());
+        }
+
+        @Override
+        public void run() {
+            if (mInfo.animInfo.applyTime(mTime, mForeground)) {
+                invalidateSelf();
+            } else {
+                reschedule();
+            }
+        }
+
+        @Override
+        public boolean setVisible(boolean visible, boolean restart) {
+            boolean result = super.setVisible(visible, restart);
+            if (visible) {
+                reschedule();
+            } else {
+                unscheduleSelf(this);
+            }
+            return result;
+        }
+
+        private void reschedule() {
+            if (!isVisible()) {
+                return;
+            }
+
+            unscheduleSelf(this);
+            final long upTime = SystemClock.uptimeMillis();
+            final long step = TICK_MS; /* tick every 200 ms */
+            scheduleSelf(this, upTime - ((upTime % step)) + step);
+        }
+
+        @Override
+        public ConstantState getConstantState() {
+            return new ClockConstantState(mInfo, isDisabled());
+        }
+
+        private static class ClockConstantState extends MyConstantState {
+
+            private final ClockBitmapInfo mInfo;
+
+            ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
+                super(info.icon, info.color, isDisabled);
+                mInfo = info;
+            }
+
+            @Override
+            public FastBitmapDrawable newDrawable() {
+                ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
+                drawable.setIsDisabled(mIsDisabled);
+                return drawable;
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/icons/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java
index 832956d..372c591 100644
--- a/src/com/android/launcher3/icons/ComponentWithLabel.java
+++ b/src/com/android/launcher3/icons/ComponentWithLabel.java
@@ -31,7 +31,7 @@
     CharSequence getLabel(PackageManager pm);
 
 
-    class ComponentCachingLogic implements CachingLogic<ComponentWithLabel> {
+    class ComponentCachingLogic<T extends ComponentWithLabel> implements CachingLogic<T> {
 
         private final PackageManager mPackageManager;
         private final boolean mAddToMemCache;
@@ -42,25 +42,23 @@
         }
 
         @Override
-        public ComponentName getComponent(ComponentWithLabel object) {
+        public ComponentName getComponent(T object) {
             return object.getComponent();
         }
 
         @Override
-        public UserHandle getUser(ComponentWithLabel object) {
+        public UserHandle getUser(T object) {
             return object.getUser();
         }
 
         @Override
-        public CharSequence getLabel(ComponentWithLabel object) {
+        public CharSequence getLabel(T object) {
             return object.getLabel(mPackageManager);
         }
 
         @Override
-        public void loadIcon(Context context,
-                ComponentWithLabel object, BitmapInfo target) {
-            // Do not load icon.
-            target.icon = BitmapInfo.LOW_RES_ICON;
+        public BitmapInfo loadIcon(Context context, T object) {
+            return BitmapInfo.LOW_RES_INFO;
         }
 
         @Override
diff --git a/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java
new file mode 100644
index 0000000..248a57d
--- /dev/null
+++ b/src/com/android/launcher3/icons/ComponentWithLabelAndIcon.java
@@ -0,0 +1,54 @@
+/*
+ * 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.icons;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.LauncherAppState;
+
+/**
+ * Extension of ComponentWithLabel to also support loading icons
+ */
+public interface ComponentWithLabelAndIcon extends ComponentWithLabel {
+
+    /**
+     * Provide an icon for this object
+     */
+    Drawable getFullResIcon(IconCache cache);
+
+    class ComponentWithIconCachingLogic extends ComponentCachingLogic<ComponentWithLabelAndIcon> {
+
+        public ComponentWithIconCachingLogic(Context context, boolean addToMemCache) {
+            super(context, addToMemCache);
+        }
+
+        @NonNull
+        @Override
+        public BitmapInfo loadIcon(Context context, ComponentWithLabelAndIcon object) {
+            Drawable d = object.getFullResIcon(LauncherAppState.getInstance(context)
+                    .getIconCache());
+            if (d == null) {
+                return super.loadIcon(context, object);
+            }
+            try (LauncherIcons li = LauncherIcons.obtain(context)) {
+                return li.createBadgedIconBitmap(d, object.getUser(), 0);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 9c46260..ff0f773 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -19,13 +19,17 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+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.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Process;
@@ -34,24 +38,26 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.IconProvider;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherFiles;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
 import com.android.launcher3.icons.cache.BaseIconCache;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.icons.cache.HandlerRunnable;
-import com.android.launcher3.model.PackageItemInfo;
+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.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
 
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 /**
@@ -61,25 +67,34 @@
 
     private static final String TAG = "Launcher.IconCache";
 
+    private final Predicate<ItemInfoWithIcon> mIsUsingFallbackIconCheck = w -> w.bitmap != null
+            && w.bitmap.isNullOrLowRes() && !isDefaultIcon(w.bitmap, w.user);
+
     private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
     private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
+    private final CachingLogic<ShortcutInfo> mShortcutCachingLogic;
 
-    private final LauncherAppsCompat mLauncherApps;
-    private final UserManagerCompat mUserManager;
+    private final LauncherApps mLauncherApps;
+    private final UserCache mUserManager;
     private final InstantAppResolver mInstantAppResolver;
     private final IconProvider mIconProvider;
 
     private int mPendingIconRequestCount = 0;
 
-    public IconCache(Context context, InvariantDeviceProfile inv) {
-        super(context, LauncherFiles.APP_ICONS_DB, MODEL_EXECUTOR.getLooper(),
-                inv.fillResIconDpi, inv.iconBitmapSize, true /* inMemoryCache */);
+    public IconCache(Context context, InvariantDeviceProfile idp) {
+        this(context, idp, LauncherFiles.APP_ICONS_DB);
+    }
+
+    public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName) {
+        super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
+                idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */);
         mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
         mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
-        mLauncherApps = LauncherAppsCompat.getInstance(mContext);
-        mUserManager = UserManagerCompat.getInstance(mContext);
+        mShortcutCachingLogic = new ShortcutCachingLogic();
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mUserManager = UserCache.INSTANCE.get(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
-        mIconProvider = IconProvider.INSTANCE.get(context);
+        mIconProvider = new IconProvider(context);
     }
 
     @Override
@@ -159,7 +174,7 @@
         CacheEntry entry = cacheLocked(application.componentName,
                 application.user, () -> null, mLauncherActivityInfoCachingLogic,
                 false, application.usingLowResIcon());
-        if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
+        if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) {
             applyCacheEntry(entry, application);
         }
     }
@@ -174,6 +189,85 @@
     }
 
     /**
+     * Fill in {@param info} with the icon for {@param si}
+     */
+    public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
+        getShortcutIcon(info, si, true, mIsUsingFallbackIconCheck);
+    }
+
+    /**
+     * Fill in {@param info} with an unbadged icon for {@param si}
+     */
+    public void getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
+        getShortcutIcon(info, si, false, mIsUsingFallbackIconCheck);
+    }
+
+    /**
+     * Fill in {@param info} with the icon and label for {@param si}. If the icon is not
+     * available, and fallback check returns true, it keeps the old icon.
+     */
+    public <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
+            @NonNull Predicate<T> fallbackIconCheck) {
+        getShortcutIcon(info, si, true /* use badged */, fallbackIconCheck);
+    }
+
+    private synchronized <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
+            boolean useBadged, @NonNull Predicate<T> fallbackIconCheck) {
+        BitmapInfo bitmapInfo;
+        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName, si.getUserHandle(),
+                    () -> si, mShortcutCachingLogic, false, false).bitmap;
+        } else {
+            // If caching is disabled, load the full icon
+            bitmapInfo = mShortcutCachingLogic.loadIcon(mContext, si);
+        }
+        if (bitmapInfo.isNullOrLowRes()) {
+            bitmapInfo = getDefaultIcon(si.getUserHandle());
+        }
+
+        if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) {
+            return;
+        }
+        info.bitmap = bitmapInfo;
+        if (useBadged) {
+            BitmapInfo badgeInfo = getShortcutInfoBadge(si);
+            try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
+                info.bitmap = li.badgeBitmap(info.bitmap.icon, badgeInfo);
+            }
+        }
+    }
+
+    /**
+     * Returns the badging info for the shortcut
+     */
+    public BitmapInfo getShortcutInfoBadge(ShortcutInfo shortcutInfo) {
+        ComponentName cn = shortcutInfo.getActivity();
+        if (cn != null) {
+            // Get the app info for the source activity.
+            AppInfo appInfo = new AppInfo();
+            appInfo.user = shortcutInfo.getUserHandle();
+            appInfo.componentName = cn;
+            appInfo.intent = new Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_LAUNCHER)
+                    .setComponent(cn);
+            getTitleAndIcon(appInfo, false);
+            return appInfo.bitmap;
+        } else {
+            PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage());
+            getTitleAndIconForApp(pkgInfo, false);
+            return pkgInfo.bitmap;
+        }
+    }
+
+    /**
+     * Fill in info with the icon and label for deep shortcut.
+     */
+    public synchronized CacheEntry getDeepShortcutTitleAndIcon(ShortcutInfo info) {
+        return cacheLocked(ShortcutKey.fromInfo(info).componentName, info.getUserHandle(),
+                () -> info, mShortcutCachingLogic, false, false);
+    }
+
+    /**
      * Fill in {@param info} with the icon and label. If the
      * corresponding activity is not found, it reverts to the package icon.
      */
@@ -181,7 +275,7 @@
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
         if (info.getTargetComponent() == null) {
-            info.applyFrom(getDefaultIcon(info.user));
+            info.bitmap = getDefaultIcon(info.user);
             info.title = "";
             info.contentDescription = "";
         } else {
@@ -224,15 +318,15 @@
     protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
         info.title = Utilities.trim(entry.title);
         info.contentDescription = entry.contentDescription;
-        info.applyFrom((entry.icon == null) ? getDefaultIcon(info.user) : entry);
+        info.bitmap = (entry.bitmap == null) ? getDefaultIcon(info.user) : entry.bitmap;
     }
 
     public Drawable getFullResIcon(LauncherActivityInfo info) {
-        return getFullResIcon(info, true);
+        return mIconProvider.getIcon(info, mIconDpi);
     }
 
-    public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
-        return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
+    public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
+        cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(), info.getAppLabel());
     }
 
     @Override
@@ -241,6 +335,15 @@
                 + ",flags_asi:" + FeatureFlags.APP_SEARCH_IMPROVEMENTS.get();
     }
 
+    @Override
+    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+        if (mIconProvider.isClockIcon(cacheKey)) {
+            // For clock icon, we always load the dynamic icon
+            return false;
+        }
+        return super.getEntryFromDB(cacheKey, entry, lowRes);
+    }
+
     public static abstract class IconLoadRequest extends HandlerRunnable {
         IconLoadRequest(Handler handler, Runnable endRunnable) {
             super(handler, endRunnable);
diff --git a/src/com/android/launcher3/icons/IconProvider.java b/src/com/android/launcher3/icons/IconProvider.java
new file mode 100644
index 0000000..1468b27
--- /dev/null
+++ b/src/com/android/launcher3/icons/IconProvider.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.icons;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Calendar;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * Class to handle icon loading from different packages
+ */
+public class IconProvider {
+
+    private static final String TAG = "IconProvider";
+    private static final boolean DEBUG = false;
+
+    private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
+
+    private static final String SYSTEM_STATE_SEPARATOR = " ";
+
+    // Default value returned if there are problems getting resources.
+    private static final int NO_ID = 0;
+
+    private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
+            LauncherActivityInfo::getIcon;
+
+    private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
+            ActivityInfo::loadUnbadgedIcon;
+
+
+    private final Context mContext;
+    private final ComponentName mCalendar;
+    private final ComponentName mClock;
+
+    public IconProvider(Context context) {
+        mContext = context;
+        mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        mClock = parseComponentOrNull(context, R.string.clock_component_name);
+    }
+
+    /**
+     * Adds any modification to the provided systemState for dynamic icons. This system state
+     * is used by caches to check for icon invalidation.
+     */
+    public String getSystemStateForPackage(String systemState, String packageName) {
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            return systemState + SYSTEM_STATE_SEPARATOR + getDay();
+        } else {
+            return systemState;
+        }
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
+     * on the UI
+     */
+    public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
+        Drawable icon = getIcon(info, iconDpi);
+        if (icon instanceof BitmapInfo.Extender) {
+            ((Extender) icon).prepareToDrawOnUi();
+        }
+        return icon;
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo
+     */
+    public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
+        return getIcon(info.getApplicationInfo().packageName, info.getUser(),
+                info, iconDpi, LAI_LOADER);
+    }
+
+    /**
+     * Loads the icon for the provided activity info
+     */
+    public Drawable getIcon(ActivityInfo info, UserHandle user) {
+        return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
+                AI_LOADER);
+    }
+
+    private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
+            BiFunction<T, P, Drawable> loader) {
+        Drawable icon = null;
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            icon = loadCalendarDrawable(0);
+        } else if (mClock != null
+                && mClock.getPackageName().equals(packageName)
+                && Process.myUserHandle().equals(user)) {
+            icon = loadClockDrawable(0);
+        }
+        return icon == null ? loader.apply(obj, param) : icon;
+    }
+
+    private Drawable loadCalendarDrawable(int iconDpi) {
+        PackageManager pm = mContext.getPackageManager();
+        try {
+            final Bundle metadata = pm.getActivityInfo(
+                    mCalendar,
+                    PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
+                    .metaData;
+            final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
+            final int id = getDynamicIconId(metadata, resources);
+            if (id != NO_ID) {
+                if (DEBUG) Log.d(TAG, "Got icon #" + id);
+                return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "Could not get activityinfo or resources for package: "
+                        + mCalendar.getPackageName());
+            }
+        }
+        return null;
+    }
+
+    private Drawable loadClockDrawable(int iconDpi) {
+        return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
+    }
+
+    protected boolean isClockIcon(ComponentKey key) {
+        return mClock != null && mClock.equals(key.componentName)
+                && Process.myUserHandle().equals(key.user);
+    }
+
+    /**
+     * @param metadata metadata of the default activity of Calendar
+     * @param resources from the Calendar package
+     * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
+     */
+    private int getDynamicIconId(Bundle metadata, Resources resources) {
+        if (metadata == null) {
+            return NO_ID;
+        }
+        String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
+        final int arrayId = metadata.getInt(key, NO_ID);
+        if (arrayId == NO_ID) {
+            return NO_ID;
+        }
+        try {
+            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
+        } catch (Resources.NotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
+            }
+            return NO_ID;
+        }
+    }
+
+    /**
+     * @return Today's day of the month, zero-indexed.
+     */
+    private int getDay() {
+        return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
+    }
+
+
+    /**
+     * Registers a callback to listen for calendar icon changes.
+     * The callback receives the packageName for the calendar icon
+     */
+    public static SafeCloseable registerIconChangeListener(Context context,
+            BiConsumer<String, UserHandle> callback, Handler handler) {
+        ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+
+        if (calendar == null && clock == null) {
+            return () -> { };
+        }
+
+        BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+        if (calendar != null) {
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_DATE_CHANGED);
+        }
+        context.registerReceiver(receiver, filter, null, handler);
+
+        return () -> context.unregisterReceiver(receiver);
+    }
+
+    private static class DateTimeChangeReceiver extends BroadcastReceiver {
+
+        private final BiConsumer<String, UserHandle> mCallback;
+
+        DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+                ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+                if (clock != null) {
+                    mCallback.accept(clock.getPackageName(), Process.myUserHandle());
+                }
+            }
+
+            ComponentName calendar =
+                    parseComponentOrNull(context, R.string.calendar_component_name);
+            if (calendar != null) {
+                for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
+                    mCallback.accept(calendar.getPackageName(), user);
+                }
+            }
+
+        }
+    }
+
+    private static ComponentName parseComponentOrNull(Context context, int resId) {
+        String cn = context.getString(resId);
+        return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
+
+    }
+}
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
index f9a94da..93de35a 100644
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
@@ -20,7 +20,6 @@
 import android.content.pm.LauncherActivityInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.IconProvider;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -55,13 +54,11 @@
     }
 
     @Override
-    public void loadIcon(Context context, LauncherActivityInfo object,
-            BitmapInfo target) {
-        LauncherIcons li = LauncherIcons.obtain(context);
-        li.createBadgedIconBitmap(
-                IconProvider.INSTANCE.get(context)
-                        .getIcon(object, li.mFillResIconDpi, true /* flattenDrawable */),
-                object.getUser(), object.getApplicationInfo().targetSdkVersion).applyTo(target);
-        li.recycle();
+    public BitmapInfo loadIcon(Context context, LauncherActivityInfo object) {
+        try (LauncherIcons li = LauncherIcons.obtain(context)) {
+            return li.createBadgedIconBitmap(new IconProvider(context)
+                            .getIcon(object, li.mFillResIconDpi),
+                    object.getUser(), object.getApplicationInfo().targetSdkVersion);
+        }
     }
 }
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index adc92c4..bf7897e 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -16,28 +16,11 @@
 
 package com.android.launcher3.icons;
 
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.os.Process;
 
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.model.PackageItemInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.util.Themes;
-
-import java.util.function.Supplier;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
 
 /**
  * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
@@ -45,8 +28,6 @@
  */
 public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
 
-    private static final String EXTRA_BADGEPKG = "badge_package";
-
     private static final Object sPoolSync = new Object();
     private static LauncherIcons sPool;
     private static int sPoolId = 0;
@@ -60,6 +41,11 @@
      * avoid allocating new objects in many cases.
      */
     public static LauncherIcons obtain(Context context, boolean shapeDetection) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return ((LauncherPreviewRenderer.PreviewContext) context).newLauncherIcons(context,
+                    shapeDetection);
+        }
+
         int poolId;
         synchronized (sPoolSync) {
             if (sPool != null) {
@@ -71,7 +57,7 @@
             poolId = sPoolId;
         }
 
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
         return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId,
                 shapeDetection);
     }
@@ -87,7 +73,7 @@
 
     private LauncherIcons next;
 
-    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId,
+    protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId,
             boolean shapeDetection) {
         super(context, fillResIconDpi, iconBitmapSize, shapeDetection);
         mPoolId = poolId;
@@ -113,86 +99,4 @@
     public void close() {
         recycle();
     }
-
-    // below methods should also migrate to BaseIconFactory
-
-    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo) {
-        return createShortcutIcon(shortcutInfo, true /* badged */);
-    }
-
-    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo, boolean badged) {
-        return createShortcutIcon(shortcutInfo, badged, null);
-    }
-
-    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo,
-            boolean badged, @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
-        Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext)
-                .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
-        IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
-
-        final Bitmap unbadgedBitmap;
-        if (unbadgedDrawable != null) {
-            unbadgedBitmap = createScaledBitmapWithoutShadow(unbadgedDrawable, 0);
-        } else {
-            if (fallbackIconProvider != null) {
-                // Fallback icons are already badged and with appropriate shadow
-                ItemInfoWithIcon fullIcon = fallbackIconProvider.get();
-                if (fullIcon != null && fullIcon.iconBitmap != null) {
-                    BitmapInfo result = new BitmapInfo();
-                    result.icon = fullIcon.iconBitmap;
-                    result.color = fullIcon.iconColor;
-                    return result;
-                }
-            }
-            unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon;
-        }
-
-        BitmapInfo result = new BitmapInfo();
-        if (!badged) {
-            result.color = Themes.getColorAccent(mContext);
-            result.icon = unbadgedBitmap;
-            return result;
-        }
-
-        final Bitmap unbadgedfinal = unbadgedBitmap;
-        final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache);
-
-        result.color = badge.iconColor;
-        result.icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
-            getShadowGenerator().recreateIcon(unbadgedfinal, c);
-            badgeWithDrawable(c, new FastBitmapDrawable(badge));
-        });
-        return result;
-    }
-
-    public ItemInfoWithIcon getShortcutInfoBadge(ShortcutInfo shortcutInfo, IconCache cache) {
-        ComponentName cn = shortcutInfo.getActivity();
-        String badgePkg = getBadgePackage(shortcutInfo);
-        boolean hasBadgePkgSet = !badgePkg.equals(shortcutInfo.getPackage());
-        if (cn != null && !hasBadgePkgSet) {
-            // Get the app info for the source activity.
-            AppInfo appInfo = new AppInfo();
-            appInfo.user = shortcutInfo.getUserHandle();
-            appInfo.componentName = cn;
-            appInfo.intent = new Intent(Intent.ACTION_MAIN)
-                    .addCategory(Intent.CATEGORY_LAUNCHER)
-                    .setComponent(cn);
-            cache.getTitleAndIcon(appInfo, false);
-            return appInfo;
-        } else {
-            PackageItemInfo pkgInfo = new PackageItemInfo(badgePkg);
-            cache.getTitleAndIconForApp(pkgInfo, false);
-            return pkgInfo;
-        }
-    }
-
-    private String getBadgePackage(ShortcutInfo si) {
-        String whitelistedPkg = mContext.getString(R.string.shortcutinfo_badgepkg_whitelist);
-        if (whitelistedPkg.equals(si.getPackage())
-                && si.getExtras() != null
-                && si.getExtras().containsKey(EXTRA_BADGEPKG)) {
-            return si.getExtras().getString(EXTRA_BADGEPKG);
-        }
-        return si.getPackage();
-    }
 }
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
new file mode 100644
index 0000000..d7eed06
--- /dev/null
+++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.icons;
+
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInfo;
+import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.Themes;
+
+/**
+ * Caching logic for shortcuts.
+ */
+public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {
+
+    private static final String TAG = "ShortcutCachingLogic";
+
+    @Override
+    public ComponentName getComponent(ShortcutInfo info) {
+        return ShortcutKey.fromInfo(info).componentName;
+    }
+
+    @Override
+    public UserHandle getUser(ShortcutInfo info) {
+        return info.getUserHandle();
+    }
+
+    @Override
+    public CharSequence getLabel(ShortcutInfo info) {
+        return info.getShortLabel();
+    }
+
+    @Override
+    public CharSequence getDescription(ShortcutInfo object, CharSequence fallback) {
+        CharSequence label = object.getLongLabel();
+        return TextUtils.isEmpty(label) ? fallback : label;
+    }
+
+    @NonNull
+    @Override
+    public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
+        try (LauncherIcons li = LauncherIcons.obtain(context)) {
+            Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
+                    context, info, LauncherAppState.getIDP(context).fillResIconDpi);
+            if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
+            return new BitmapInfo(li.createScaledBitmapWithoutShadow(
+                    unbadgedDrawable, 0), Themes.getColorAccent(context));
+        }
+    }
+
+    @Override
+    public long getLastUpdatedTime(ShortcutInfo shortcutInfo, PackageInfo info) {
+        if (shortcutInfo == null || !FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            return info.lastUpdateTime;
+        }
+        return Math.max(shortcutInfo.getLastChangedTimestamp(), info.lastUpdateTime);
+    }
+
+    @Override
+    public boolean addToMemCache() {
+        return false;
+    }
+
+    /**
+     * Similar to {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)} with additional
+     * Launcher specific checks
+     */
+    public static Drawable getIcon(Context context, ShortcutInfo shortcutInfo, int density) {
+        if (GO_DISABLE_WIDGETS) {
+            return null;
+        }
+        try {
+            return context.getSystemService(LauncherApps.class)
+                    .getShortcutIconDrawable(shortcutInfo, density);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to get shortcut icon", e);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
index 938955c..800598e 100644
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
@@ -24,9 +24,9 @@
 import android.widget.PopupMenu;
 import android.widget.PopupMenu.OnMenuItemClickListener;
 
-import com.android.launcher3.ItemInfo;
 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;
diff --git a/src/com/android/launcher3/logging/DumpTargetWrapper.java b/src/com/android/launcher3/logging/DumpTargetWrapper.java
deleted file mode 100644
index 067bdfd..0000000
--- a/src/com/android/launcher3/logging/DumpTargetWrapper.java
+++ /dev/null
@@ -1,169 +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.logging;
-
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-
-import android.content.ComponentName;
-import android.os.Process;
-import android.text.TextUtils;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.model.nano.LauncherDumpProto;
-import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
-import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
-import com.android.launcher3.model.nano.LauncherDumpProto.ItemType;
-import com.android.launcher3.model.nano.LauncherDumpProto.UserType;
-import com.android.launcher3.util.ShortcutUtil;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * This class can be used when proto definition doesn't support nesting.
- */
-public class DumpTargetWrapper {
-    DumpTarget node;
-    ArrayList<DumpTargetWrapper> children;
-
-    public DumpTargetWrapper() {
-        children = new ArrayList<>();
-    }
-
-    public DumpTargetWrapper(int containerType, int id) {
-        this();
-        node = newContainerTarget(containerType, id);
-    }
-
-    public DumpTargetWrapper(ItemInfo info) {
-        this();
-        node = newItemTarget(info);
-    }
-
-    public DumpTarget getDumpTarget() {
-        return node;
-    }
-
-    public void add(DumpTargetWrapper child) {
-        children.add(child);
-    }
-
-    public List<DumpTarget> getFlattenedList() {
-        ArrayList<DumpTarget> list = new ArrayList<>();
-        list.add(node);
-        if (!children.isEmpty()) {
-            for(DumpTargetWrapper t: children) {
-                list.addAll(t.getFlattenedList());
-            }
-            list.add(node); // add a delimiter empty object
-        }
-        return list;
-    }
-    public DumpTarget newItemTarget(ItemInfo info) {
-        DumpTarget dt = new DumpTarget();
-        dt.type = DumpTarget.Type.ITEM;
-        if (info == null) {
-            return dt;
-        }
-        switch (info.itemType) {
-            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                dt.itemType = ItemType.APP_ICON;
-                break;
-            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                dt.itemType = ItemType.WIDGET;
-                break;
-            case ITEM_TYPE_DEEP_SHORTCUT:
-            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-                dt.itemType = ItemType.SHORTCUT;
-                break;
-            default:
-                dt.itemType = ItemType.UNKNOWN_ITEMTYPE;
-                break;
-        }
-        return dt;
-    }
-
-    public DumpTarget newContainerTarget(int type, int id) {
-        DumpTarget dt = new DumpTarget();
-        dt.type = DumpTarget.Type.CONTAINER;
-        dt.containerType = type;
-        dt.pageId = id;
-        return dt;
-    }
-
-    public static String getDumpTargetStr(DumpTarget t) {
-        if (t == null){
-            return "";
-        }
-        switch (t.type) {
-            case LauncherDumpProto.DumpTarget.Type.ITEM:
-                return getItemStr(t);
-            case LauncherDumpProto.DumpTarget.Type.CONTAINER:
-                String str = LoggerUtils.getFieldName(t.containerType, ContainerType.class);
-                if (t.containerType == ContainerType.WORKSPACE) {
-                    str += " id=" + t.pageId;
-                } else if (t.containerType == ContainerType.FOLDER) {
-                    str += " grid(" + t.gridX + "," + t.gridY+ ")";
-                }
-                return str;
-            default:
-                return "UNKNOWN TARGET TYPE";
-        }
-    }
-
-    private static String getItemStr(DumpTarget t) {
-        if (t == null) {
-            return "";
-        }
-        String typeStr = LoggerUtils.getFieldName(t.itemType, ItemType.class);
-        if (!TextUtils.isEmpty(t.packageName)) {
-            typeStr += ", package=" + t.packageName;
-        }
-        if (!TextUtils.isEmpty(t.component)) {
-            typeStr += ", component=" + t.component;
-        }
-        return typeStr + ", grid(" + t.gridX + "," + t.gridY + "), span(" + t.spanX + "," + t.spanY
-                + "), pageIdx=" + t.pageId + " user=" + t.userType;
-    }
-
-    public DumpTarget writeToDumpTarget(ItemInfo info) {
-        if (info == null) {
-            return node;
-        }
-        if (ShortcutUtil.isDeepShortcut(info)) {
-            node.component = ((WorkspaceItemInfo) info).getDeepShortcutId();
-        } else {
-            ComponentName cmp = info.getTargetComponent();
-            node.component = cmp == null ? "" : cmp.flattenToString();
-        }
-        node.packageName = info.getTargetComponent() == null? "":
-                info.getTargetComponent().getPackageName();
-        if (info instanceof LauncherAppWidgetInfo) {
-            node.component = ((LauncherAppWidgetInfo) info).providerName.flattenToString();
-            node.packageName = ((LauncherAppWidgetInfo) info).providerName.getPackageName();
-        }
-
-        node.gridX = info.cellX;
-        node.gridY = info.cellY;
-        node.spanX = info.spanX;
-        node.spanY = info.spanY;
-        node.userType = (info.user.equals(Process.myUserHandle()))? UserType.DEFAULT : UserType.WORK;
-        return node;
-    }
-}
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 923a89b..6bc1ecb 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -8,6 +8,9 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.IOUtils;
 
 import java.io.BufferedReader;
@@ -35,11 +38,13 @@
     private static final DateFormat DATE_FORMAT =
             DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
 
-    private static final long MAX_LOG_FILE_SIZE = 4 << 20;  // 4 mb
+    private static final long MAX_LOG_FILE_SIZE = 8 << 20;  // 4 mb
 
     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 void setDir(File logsDir) {
         if (ENABLED) {
             synchronized (DATE_FORMAT) {
@@ -88,7 +93,8 @@
         Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
     }
 
-    private static Handler getHandler() {
+    @VisibleForTesting
+    static Handler getHandler() {
         synchronized (DATE_FORMAT) {
             if (sHandler == null) {
                 sHandler = new Handler(createAndStartNewLooper("file-logger"),
@@ -102,15 +108,16 @@
      * Blocks until all the pending logs are written to the disk
      * @param out if not null, all the persisted logs are copied to the writer.
      */
-    public static void flushAll(PrintWriter out) throws InterruptedException {
+    public static boolean flushAll(PrintWriter out) throws InterruptedException {
         if (!ENABLED) {
-            return;
+            return false;
         }
         CountDownLatch latch = new CountDownLatch(1);
         Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
                 Pair.create(out, latch)).sendToTarget();
 
         latch.await(2, TimeUnit.SECONDS);
+        return latch.getCount() == 0;
     }
 
     /**
@@ -143,7 +150,7 @@
                 case MSG_WRITE: {
                     Calendar cal = Calendar.getInstance();
                     // suffix with 0 or 1 based on the day of the year.
-                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS);
 
                     if (!fileName.equals(mCurrentFileName)) {
                         closeWriter();
@@ -191,8 +198,9 @@
                             (Pair<PrintWriter, CountDownLatch>) msg.obj;
 
                     if (p.first != null) {
-                        dumpFile(p.first, FILE_NAME_PREFIX + 0);
-                        dumpFile(p.first, FILE_NAME_PREFIX + 1);
+                        for (int i = 0; i < LOG_DAYS; i++) {
+                            dumpFile(p.first, FILE_NAME_PREFIX + i);
+                        }
                     }
                     p.second.countDown();
                     return true;
@@ -222,4 +230,18 @@
             }
         }
     }
+
+    /**
+     * Gets files used for FileLog
+     */
+    public static File[] getLogFiles() {
+        try {
+            flushAll(null);
+        } catch (InterruptedException e) { }
+        File[] files = new File[LOG_DAYS];
+        for (int i = 0; i < LOG_DAYS; i++) {
+            files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i);
+        }
+        return files;
+    }
 }
diff --git a/src/com/android/launcher3/logging/InstanceId.java b/src/com/android/launcher3/logging/InstanceId.java
new file mode 100644
index 0000000..e720d75
--- /dev/null
+++ b/src/com/android/launcher3/logging/InstanceId.java
@@ -0,0 +1,108 @@
+/*
+ * 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.logging;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+
+/**
+ * An opaque identifier used to disambiguate which logs refer to a particular instance of some
+ * UI element. Useful when there might be multiple instances simultaneously active.
+ * Obtain from InstanceIdSequence.  Clipped to range [0, INSTANCE_ID_MAX].
+ *
+ * Copy of frameworks/base/core/java/com/android/internal/logging/InstanceId.java.
+ */
+public final class InstanceId implements Parcelable {
+    // At most 20 bits: ~1m possibilities, ~0.5% probability of collision in 100 values
+    static final int INSTANCE_ID_MAX = 1 << 20;
+
+    private final int mId;
+    InstanceId(int id) {
+        mId = min(max(0, id), INSTANCE_ID_MAX);
+    }
+
+    private InstanceId(Parcel in) {
+        this(in.readInt());
+    }
+
+    @VisibleForTesting
+    public int getId() {
+        return mId;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return mId + "";
+    }
+
+    /**
+     * Create a fake instance ID for testing purposes.  Not for production use. See also
+     * InstanceIdSequenceFake, which is a testing replacement for InstanceIdSequence.
+     * @param id The ID you want to assign.
+     * @return new InstanceId.
+     */
+    @VisibleForTesting
+    public static InstanceId fakeInstanceId(int id) {
+        return new InstanceId(id);
+    }
+
+    @Override
+    public int hashCode() {
+        return mId;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof InstanceId)) {
+            return false;
+        }
+        return mId == ((InstanceId) obj).mId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mId);
+    }
+
+    public static final Parcelable.Creator<InstanceId> CREATOR =
+            new Parcelable.Creator<InstanceId>() {
+                @Override
+                public InstanceId createFromParcel(Parcel in) {
+                    return new InstanceId(in);
+                }
+
+                @Override
+                public InstanceId[] newArray(int size) {
+                    return new InstanceId[size];
+                }
+            };
+
+}
diff --git a/src/com/android/launcher3/logging/InstanceIdSequence.java b/src/com/android/launcher3/logging/InstanceIdSequence.java
new file mode 100644
index 0000000..ee6a5a4
--- /dev/null
+++ b/src/com/android/launcher3/logging/InstanceIdSequence.java
@@ -0,0 +1,71 @@
+/*
+ * 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.logging;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * Generates random InstanceIds in range [1, instanceIdMax] for passing to
+ * UiEventLogger.logWithInstanceId(). Holds a SecureRandom, which self-seeds on first use; try to
+ * give it a long lifetime. Safe for concurrent use.
+ *
+ * Copy of frameworks/base/core/java/com/android/internal/logging/InstanceIdSequence.java
+ */
+public class InstanceIdSequence {
+    protected final int mInstanceIdMax;
+    private final Random mRandom = new SecureRandom();
+
+    /**
+     * Constructs a sequence with identifiers [1, instanceIdMax].  Capped at INSTANCE_ID_MAX.
+     * @param instanceIdMax Limiting value of identifiers. Normally positive: otherwise you get
+     *                      an all-1 sequence.
+     */
+    public InstanceIdSequence(int instanceIdMax) {
+        mInstanceIdMax = min(max(1, instanceIdMax), InstanceId.INSTANCE_ID_MAX);
+    }
+
+    /**
+     * Constructs a sequence with identifiers [1, InstanceId.INSTANCE_ID_MAX].
+     */
+    public InstanceIdSequence() {
+        this(InstanceId.INSTANCE_ID_MAX);
+    }
+
+    /**
+     * Gets the next instance from the sequence.  Safe for concurrent use.
+     * @return new InstanceId
+     */
+    public InstanceId newInstanceId() {
+        return newInstanceIdInternal(1 + mRandom.nextInt(mInstanceIdMax));
+    }
+
+    /**
+     * Factory function for instance IDs, used for testing.
+     * @param id
+     * @return new InstanceId(id)
+     */
+    @VisibleForTesting
+    protected InstanceId newInstanceIdInternal(int id) {
+        return new InstanceId(id);
+    }
+}
diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java
index 925b7ba..cd4f034 100644
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ b/src/com/android/launcher3/logging/LoggerUtils.java
@@ -15,28 +15,24 @@
  */
 package com.android.launcher3.logging;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.NAVBAR;
-
 import android.util.ArrayMap;
 import android.util.SparseArray;
 import android.view.View;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.ButtonDropTarget;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogExtensions.TargetExtension;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.userevent.nano.LauncherLogProto.TipType;
 import com.android.launcher3.util.InstantAppResolver;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.ArrayList;
 
 /**
  * Helper methods for logging.
@@ -44,7 +40,8 @@
 public class LoggerUtils {
     private static final ArrayMap<Class, SparseArray<String>> sNameCache = new ArrayMap<>();
     private static final String UNKNOWN = "UNKNOWN";
-    private static final int DEFAULT_PREDICTED_RANK = -100;
+    private static final int DEFAULT_PREDICTED_RANK = 10000;
+    private static final String DELIMITER_DOT = "\\.";
 
     public static String getFieldName(int value, Class c) {
         SparseArray<String> cache;
@@ -69,91 +66,6 @@
         return result != null ? result : UNKNOWN;
     }
 
-    public static String getActionStr(Action action) {
-        String str = "";
-        switch (action.type) {
-            case Action.Type.TOUCH:
-                str += getFieldName(action.touch, Action.Touch.class);
-                if (action.touch == Action.Touch.SWIPE || action.touch == Action.Touch.FLING) {
-                    str += " direction=" + getFieldName(action.dir, Action.Direction.class);
-                }
-                break;
-            case Action.Type.COMMAND:
-                str += getFieldName(action.command, Action.Command.class);
-                break;
-            default: return getFieldName(action.type, Action.Type.class);
-        }
-        if (action.touch == Action.Touch.SWIPE || action.touch == Action.Touch.FLING ||
-                (action.command == Action.Command.BACK && action.dir != Action.Direction.NONE)) {
-            str += " direction=" + getFieldName(action.dir, Action.Direction.class);
-        }
-        return str;
-    }
-
-    public static String getTargetStr(Target t) {
-        if (t == null) {
-            return "";
-        }
-        String str = "";
-        switch (t.type) {
-            case Target.Type.ITEM:
-                str = getItemStr(t);
-                break;
-            case Target.Type.CONTROL:
-                str = getFieldName(t.controlType, ControlType.class);
-                break;
-            case Target.Type.CONTAINER:
-                str = getFieldName(t.containerType, ContainerType.class);
-                if (t.containerType == ContainerType.WORKSPACE ||
-                        t.containerType == ContainerType.HOTSEAT ||
-                        t.containerType == NAVBAR) {
-                    str += " id=" + t.pageIndex;
-                } else if (t.containerType == ContainerType.FOLDER) {
-                    str += "[PageIndex=" + t.pageIndex + ", grid(" + t.gridX + "," + t.gridY + ")]";
-                }
-                break;
-            default:
-                str += "UNKNOWN TARGET TYPE";
-        }
-
-        if (t.spanX != 1 || t.spanY != 1) {
-            str += " span(" + t.spanX + "," + t.spanY + ")";
-        }
-
-        if (t.tipType != TipType.DEFAULT_NONE) {
-            str += " " + getFieldName(t.tipType, TipType.class);
-        }
-
-        return str;
-    }
-
-    private static String getItemStr(Target t) {
-        String typeStr = getFieldName(t.itemType, ItemType.class);
-        if (t.packageNameHash != 0) {
-            typeStr += ", packageHash=" + t.packageNameHash;
-        }
-        if (t.componentHash != 0) {
-            typeStr += ", componentHash=" + t.componentHash;
-        }
-        if (t.intentHash != 0) {
-            typeStr += ", intentHash=" + t.intentHash;
-        }
-        if (t.itemType == ItemType.FOLDER_ICON) {
-            typeStr += ", grid(" + t.gridX + "," + t.gridY + ")";
-        } else if ((t.packageNameHash != 0 || t.componentHash != 0 || t.intentHash != 0)
-                && t.itemType != ItemType.TASK) {
-            typeStr += ", predictiveRank=" + t.predictedRank + ", grid(" + t.gridX + "," + t.gridY
-                    + "), span(" + t.spanX + "," + t.spanY + "), pageIdx=" + t.pageIndex;
-        }
-        if (t.searchQueryLength != 0) {
-            typeStr += ", searchQueryLength=" + t.searchQueryLength;
-        }
-        if (t.itemType == ItemType.TASK) {
-            typeStr += ", pageIdx=" + t.pageIndex;
-        }
-        return typeStr;
-    }
-
     public static Target newItemTarget(int itemType) {
         Target t = newTarget(Target.Type.ITEM);
         t.itemType = itemType;
@@ -253,4 +165,26 @@
         event.action = action;
         return event;
     }
+
+    /**
+     * Creates LauncherEvent using Action and ArrayList of Targets
+     */
+    public static LauncherEvent newLauncherEvent(Action action, ArrayList<Target> targets) {
+        Target[] targetsArray = new Target[targets.size()];
+        targets.toArray(targetsArray);
+        return newLauncherEvent(action, targetsArray);
+    }
+
+    /**
+     * String conversion for only the helpful parts of {@link Object#toString()} method
+     * @param stringToExtract "foo.bar.baz.MyObject@1234"
+     * @return "MyObject@1234"
+     */
+    public static String extractObjectNameAndAddress(String stringToExtract) {
+        String[] superStringParts = stringToExtract.split(DELIMITER_DOT);
+        if (superStringParts.length == 0) {
+            return "";
+        }
+        return superStringParts[superStringParts.length - 1];
+    }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index cad95b0..8e23b65 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -15,32 +15,410 @@
  */
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_DOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_OPEN_UP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
+
 import android.content.Context;
-import android.content.Intent;
-import android.view.View;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 
 /**
- * Handles the user event logging in Q.
+ * Handles the user event logging in R+.
+ * All of the event ids are defined here.
+ * Most of the methods are dummy methods for Launcher3
+ * Actual call happens only for Launcher variant that implements QuickStep.
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
-    protected LogStateProvider mStateProvider;
-    public static StatsLogManager newInstance(Context context, LogStateProvider stateProvider) {
+    public static final int LAUNCHER_STATE_UNSPECIFIED = 0;
+    public static final int LAUNCHER_STATE_BACKGROUND = 1;
+    public static final int LAUNCHER_STATE_HOME = 2;
+    public static final int LAUNCHER_STATE_OVERVIEW = 3;
+    public static final int LAUNCHER_STATE_ALLAPPS = 4;
+    public static final int LAUNCHER_STATE_UNCHANGED = 5;
+
+    /**
+     * Returns proper launcher state enum for {@link StatsLogManager}
+     * (to be removed during UserEventDispatcher cleanup)
+     */
+    public static int containerTypeToAtomState(int containerType) {
+        switch (containerType) {
+            case LauncherLogProto.ContainerType.ALLAPPS_VALUE:
+                return LAUNCHER_STATE_ALLAPPS;
+            case LauncherLogProto.ContainerType.OVERVIEW_VALUE:
+                return LAUNCHER_STATE_OVERVIEW;
+            case LauncherLogProto.ContainerType.WORKSPACE_VALUE:
+                return LAUNCHER_STATE_HOME;
+            case LauncherLogProto.ContainerType.APP_VALUE:
+                return LAUNCHER_STATE_BACKGROUND;
+        }
+        return LAUNCHER_STATE_UNSPECIFIED;
+    }
+
+    /**
+     * Returns event enum based on the two {@link ContainerType} transition information when
+     * swipe gesture happens.
+     * (to be removed during UserEventDispatcher cleanup)
+     */
+    public static EventEnum getLauncherAtomEvent(int startContainerType,
+            int targetContainerType, EventEnum fallbackEvent) {
+        if (startContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()) {
+            return LAUNCHER_HOME_GESTURE;
+        } else if (startContainerType != LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()) {
+            return LAUNCHER_OVERVIEW_GESTURE;
+        } else if (startContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()
+                && targetContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+            return LAUNCHER_ALLAPPS_OPEN_UP;
+        } else if (startContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()
+                && targetContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+            return LAUNCHER_ALLAPPS_CLOSE_DOWN;
+        }
+        return fallbackEvent; // TODO fix
+    }
+
+    public interface EventEnum {
+        int getId();
+    }
+
+    public enum LauncherEvent implements EventEnum {
+        /* Used to prevent double logging. */
+        IGNORE(-1),
+
+        @UiEvent(doc = "App launched from workspace, hotseat or folder in launcher")
+        LAUNCHER_APP_LAUNCH_TAP(338),
+
+        @UiEvent(doc = "Task launched from overview using TAP")
+        LAUNCHER_TASK_LAUNCH_TAP(339),
+
+        @UiEvent(doc = "User tapped on notification inside popup context menu.")
+        LAUNCHER_NOTIFICATION_LAUNCH_TAP(516),
+
+        @UiEvent(doc = "Task launched from overview using SWIPE DOWN")
+        LAUNCHER_TASK_LAUNCH_SWIPE_DOWN(340),
+
+        @UiEvent(doc = "TASK dismissed from overview using SWIPE UP")
+        LAUNCHER_TASK_DISMISS_SWIPE_UP(341),
+
+        @UiEvent(doc = "User dragged a launcher item")
+        LAUNCHER_ITEM_DRAG_STARTED(383),
+
+        @UiEvent(doc = "A dragged launcher item is successfully dropped")
+        LAUNCHER_ITEM_DROP_COMPLETED(385),
+
+        @UiEvent(doc = "A dragged launcher item is successfully dropped on another item "
+                + "resulting in a new folder creation")
+        LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
+
+        @UiEvent(doc = "Folder's label is automatically assigned.")
+        LAUNCHER_FOLDER_AUTO_LABELED(591),
+
+        @UiEvent(doc = "Could not auto-label a folder because primary suggestion is null or empty.")
+        LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY(592),
+
+        @UiEvent(doc = "Could not auto-label a folder because no suggestions exist.")
+        LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS(593),
+
+        @UiEvent(doc = "User manually updated the folder label.")
+        LAUNCHER_FOLDER_LABEL_UPDATED(460),
+
+        @UiEvent(doc = "User long pressed on the workspace empty space.")
+        LAUNCHER_WORKSPACE_LONGPRESS(461),
+
+        @UiEvent(doc = "User tapped or long pressed on a wallpaper icon inside launcher settings.")
+        LAUNCHER_WALLPAPER_BUTTON_TAP_OR_LONGPRESS(462),
+
+        @UiEvent(doc = "User tapped or long pressed on settings icon inside launcher settings.")
+        LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS(463),
+
+        @UiEvent(doc = "User tapped or long pressed on widget tray icon inside launcher settings.")
+        LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS(464),
+
+        @UiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar")
+        LAUNCHER_ITEM_DROPPED_ON_REMOVE(465),
+
+        @UiEvent(doc = "A dragged item is dropped on 'Cancel' button in the target bar")
+        LAUNCHER_ITEM_DROPPED_ON_CANCEL(466),
+
+        @UiEvent(doc = "A predicted item is dragged and dropped on 'Don't suggest app'"
+                + " button in the target bar")
+        LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST(467),
+
+        @UiEvent(doc = "A dragged item is dropped on 'Uninstall' button in target bar")
+        LAUNCHER_ITEM_DROPPED_ON_UNINSTALL(468),
+
+        @UiEvent(doc = "User completed uninstalling the package after dropping on "
+                + "the icon onto 'Uninstall' button in the target bar")
+        LAUNCHER_ITEM_UNINSTALL_COMPLETED(469),
+
+        @UiEvent(doc = "User cancelled uninstalling the package after dropping on "
+                + "the icon onto 'Uninstall' button in the target bar")
+        LAUNCHER_ITEM_UNINSTALL_CANCELLED(470),
+
+        @UiEvent(doc = "User tapped or long pressed on the task icon(aka package icon) "
+                + "from overview to open task menu.")
+        LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS(517),
+
+        @UiEvent(doc = "User opened package specific widgets list by tapping on widgets system "
+                + "shortcut inside popup context menu.")
+        LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP(514),
+
+        @UiEvent(doc = "User tapped on app info system shortcut.")
+        LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP(515),
+
+        @UiEvent(doc = "User tapped on split screen icon on a task menu.")
+        LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP(518),
+
+        @UiEvent(doc = "User tapped on free form icon on a task menu.")
+        LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP(519),
+
+        @UiEvent(doc = "User tapped on pause app system shortcut.")
+        LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521),
+
+        @UiEvent(doc = "User tapped on pin system shortcut.")
+        LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP(522),
+
+        @UiEvent(doc = "User is shown All Apps education view.")
+        LAUNCHER_ALL_APPS_EDU_SHOWN(523),
+
+        @UiEvent(doc = "User opened a folder.")
+        LAUNCHER_FOLDER_OPEN(551),
+
+        @UiEvent(doc = "Hotseat education half sheet seen")
+        LAUNCHER_HOTSEAT_EDU_SEEN(479),
+
+        @UiEvent(doc = "Hotseat migration accepted")
+        LAUNCHER_HOTSEAT_EDU_ACCEPT(480),
+
+        @UiEvent(doc = "Hotseat migration denied")
+        LAUNCHER_HOTSEAT_EDU_DENY(481),
+
+        @UiEvent(doc = "Hotseat education tip shown")
+        LAUNCHER_HOTSEAT_EDU_ONLY_TIP(482),
+
+        /**
+         * @deprecated LauncherUiChanged.rank field is repurposed to store all apps rank, so no
+         * separate event is required.
+         */
+        @Deprecated
+        @UiEvent(doc = "App launch ranking logged for all apps predictions")
+        LAUNCHER_ALL_APPS_RANKED(552),
+
+        @UiEvent(doc = "App launch ranking logged for hotseat predictions)")
+        LAUNCHER_HOTSEAT_RANKED(553),
+        @UiEvent(doc = "Launcher is now in background. e.g., Screen off event")
+        LAUNCHER_ONSTOP(562),
+
+        @UiEvent(doc = "Launcher is now in foreground. e.g., Screen on event, back button")
+        LAUNCHER_ONRESUME(563),
+
+        @UiEvent(doc = "User swipes or fling in LEFT direction on workspace.")
+        LAUNCHER_SWIPELEFT(564),
+
+        @UiEvent(doc = "User swipes or fling in RIGHT direction on workspace.")
+        LAUNCHER_SWIPERIGHT(565),
+
+        @UiEvent(doc = "User swipes or fling in UP direction in unknown way.")
+        LAUNCHER_UNKNOWN_SWIPEUP(566),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction in unknown way.")
+        LAUNCHER_UNKNOWN_SWIPEDOWN(567),
+
+        @UiEvent(doc = "User swipes or fling in UP direction to open apps drawer.")
+        LAUNCHER_ALLAPPS_OPEN_UP(568),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction to close apps drawer.")
+        LAUNCHER_ALLAPPS_CLOSE_DOWN(569),
+
+        @UiEvent(doc = "User swipes or fling in UP direction and hold from the bottom bazel area")
+        LAUNCHER_OVERVIEW_GESTURE(570),
+
+        @UiEvent(doc = "User swipes or fling in LEFT direction on the bottom bazel area.")
+        LAUNCHER_QUICKSWITCH_LEFT(571),
+
+        @UiEvent(doc = "User swipes or fling in RIGHT direction on the bottom bazel area.")
+        LAUNCHER_QUICKSWITCH_RIGHT(572),
+
+        @UiEvent(doc = "User swipes or fling in DOWN direction on the bottom bazel area.")
+        LAUNCHER_SWIPEDOWN_NAVBAR(573),
+
+        @UiEvent(doc = "User swipes or fling in UP direction from bottom bazel area.")
+        LAUNCHER_HOME_GESTURE(574),
+
+        @UiEvent(doc = "User's workspace layout information is snapshot in the background.")
+        LAUNCHER_WORKSPACE_SNAPSHOT(579),
+
+        @UiEvent(doc = "User tapped on the screenshot button on overview)")
+        LAUNCHER_OVERVIEW_ACTIONS_SCREENSHOT(580),
+
+        @UiEvent(doc = "User tapped on the select button on overview)")
+        LAUNCHER_OVERVIEW_ACTIONS_SELECT(581),
+
+        @UiEvent(doc = "User tapped on the share button on overview")
+        LAUNCHER_OVERVIEW_ACTIONS_SHARE(582),
+
+        @UiEvent(doc = "User tapped on the close button in select mode")
+        LAUNCHER_SELECT_MODE_CLOSE(583),
+
+        @UiEvent(doc = "User tapped on the highlight items in select mode")
+        LAUNCHER_SELECT_MODE_ITEM(584);
+
+        // ADD MORE
+
+        private final int mId;
+
+        LauncherEvent(int id) {
+            mId = id;
+        }
+
+        public int getId() {
+            return mId;
+        }
+    }
+
+    /**
+     * Launcher specific ranking related events.
+     */
+    public enum LauncherRankingEvent implements EventEnum {
+
+        UNKNOWN(0);
+        // ADD MORE
+
+        private final int mId;
+
+        LauncherRankingEvent(int id) {
+            mId = id;
+        }
+
+        public int getId() {
+            return mId;
+        }
+    }
+
+    /**
+     * Helps to construct and write the log message.
+     */
+    public interface StatsLogger {
+
+        /**
+         * Sets log fields from provided {@link ItemInfo}.
+         */
+        default StatsLogger withItemInfo(ItemInfo itemInfo) {
+            return this;
+        }
+
+
+        /**
+         * Sets {@link InstanceId} of log message.
+         */
+        default StatsLogger withInstanceId(InstanceId instanceId) {
+            return this;
+        }
+
+        /**
+         * Sets rank field of log message.
+         */
+        default StatsLogger withRank(int rank) {
+            return this;
+        }
+
+        /**
+         * Sets source launcher state field of log message.
+         */
+        default StatsLogger withSrcState(int srcState) {
+            return this;
+        }
+
+        /**
+         * Sets destination launcher state field of log message.
+         */
+        default StatsLogger withDstState(int dstState) {
+            return this;
+        }
+
+        /**
+         * Sets FromState field of log message.
+         */
+        default StatsLogger withFromState(FromState fromState) {
+            return this;
+        }
+
+        /**
+         * Sets ToState field of log message.
+         */
+        default StatsLogger withToState(ToState toState) {
+            return this;
+        }
+
+        /**
+         * Sets editText field of log message.
+         */
+        default StatsLogger withEditText(String editText) {
+            return this;
+        }
+
+        /**
+         * Sets the final value for container related fields of log message.
+         *
+         * By default container related fields are derived from {@link ItemInfo}, this method would
+         * override those values.
+         */
+        default StatsLogger withContainerInfo(ContainerInfo containerInfo) {
+            return this;
+        }
+
+        /**
+         * Builds the final message and logs it as {@link EventEnum}.
+         */
+        default void log(EventEnum event) {
+        }
+    }
+
+    /**
+     * Returns new logger object.
+     */
+    public StatsLogger logger() {
+        return new StatsLogger() {
+        };
+    }
+
+    /**
+     * Creates a new instance of {@link StatsLogManager} based on provided context.
+     */
+    public static StatsLogManager newInstance(Context context) {
         StatsLogManager mgr = Overrides.getObject(StatsLogManager.class,
                 context.getApplicationContext(), R.string.stats_log_manager_class);
-        mgr.mStateProvider = stateProvider;
-        mgr.verify();
         return mgr;
     }
 
-    public void logAppLaunch(View v, Intent intent) { }
-    public void logTaskLaunch(View v, ComponentKey key) { }
-    public void logTaskDismiss(View v, ComponentKey key) { }
-    public void logSwipeOnContainer(boolean isSwipingToLeft, int pageId) { }
-    public void verify() {}     // TODO: should move into robo tests
+    /**
+     * Log an event with ranked-choice information along with package. Does nothing if event.getId()
+     * <= 0.
+     *
+     * @param rankingEvent an enum implementing EventEnum interface.
+     * @param instanceId An identifier obtained from an InstanceIdSequence.
+     * @param packageName the package name of the relevant app, if known (null otherwise).
+     * @param position the position picked.
+     */
+    public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName,
+            int position) {
+    }
+
+    /**
+     * Logs snapshot, or impression of the current workspace.
+     */
+    public void logSnapshot() {
+    }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogUtils.java b/src/com/android/launcher3/logging/StatsLogUtils.java
index b02a050..a5cc7ea 100644
--- a/src/com/android/launcher3/logging/StatsLogUtils.java
+++ b/src/com/android/launcher3/logging/StatsLogUtils.java
@@ -1,48 +1,27 @@
 package com.android.launcher3.logging;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.DEFAULT_CONTAINERTYPE;
-
 import android.view.View;
 import android.view.ViewParent;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+
+import java.util.ArrayList;
 
 public class StatsLogUtils {
-
-    // Defined in android.stats.launcher.nano
-    // As they cannot be linked in this file, defining again.
-    public final static int LAUNCHER_STATE_BACKGROUND = 0;
-    public final static int LAUNCHER_STATE_HOME = 1;
-    public final static int LAUNCHER_STATE_OVERVIEW = 2;
-    public final static int LAUNCHER_STATE_ALLAPPS = 3;
-
     private final static int MAXIMUM_VIEW_HIERARCHY_LEVEL = 5;
 
-    public interface LogStateProvider {
-        int getCurrentState();
-    }
-
     /**
      * Implemented by containers to provide a container source for a given child.
-     *
-     * Currently,
      */
     public interface LogContainerProvider {
 
         /**
-         * Copies data from the source to the destination proto.
-         *
-         * @param v            source of the data
-         * @param info         source of the data
-         * @param target       dest of the data
-         * @param targetParent dest of the data
+         * Populates parent container targets for an item
          */
-        void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent);
+        void fillInLogContainerData(ItemInfo childInfo, Target child, ArrayList<Target> parents);
     }
 
     /**
@@ -67,20 +46,4 @@
         }
         return null;
     }
-
-    public static int getContainerTypeFromState(int state) {
-        int containerType = DEFAULT_CONTAINERTYPE;
-        switch (state) {
-            case StatsLogUtils.LAUNCHER_STATE_ALLAPPS:
-                containerType = ContainerType.ALLAPPS;
-                break;
-            case StatsLogUtils.LAUNCHER_STATE_HOME:
-                containerType = ContainerType.WORKSPACE;
-                break;
-            case StatsLogUtils.LAUNCHER_STATE_OVERVIEW:
-                containerType = ContainerType.OVERVIEW;
-                break;
-        }
-        return containerType;
-    }
 }
diff --git a/src/com/android/launcher3/logging/UiEvent.java b/src/com/android/launcher3/logging/UiEvent.java
new file mode 100644
index 0000000..20d6c72
--- /dev/null
+++ b/src/com/android/launcher3/logging/UiEvent.java
@@ -0,0 +1,34 @@
+/*
+ * 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.logging;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(SOURCE)
+@Target(FIELD)
+//  Copy of frameworks/base/core/java/com/android/internal/logging/UiEvent.java
+public @interface UiEvent {
+
+    /**
+     * An explanation, suitable for Android analysts, of the UI event that this log represents.
+     */
+    String doc();
+}
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 99906fe..e094cab 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -25,6 +25,9 @@
 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
 import static com.android.launcher3.logging.LoggerUtils.newTarget;
 import static com.android.launcher3.logging.LoggerUtils.newTouchAction;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.TipType;
 
 import static java.util.Optional.ofNullable;
 
@@ -33,7 +36,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.os.Process;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
 
@@ -41,12 +46,11 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
@@ -55,7 +59,11 @@
 import com.android.launcher3.util.LogConfig;
 import com.android.launcher3.util.ResourceBasedOverride;
 
-import java.util.Locale;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+import com.google.protobuf.nano.MessageNano;
+
+import java.util.ArrayList;
 import java.util.UUID;
 
 /**
@@ -67,12 +75,13 @@
 public class UserEventDispatcher implements ResourceBasedOverride {
 
     private static final String TAG = "UserEvent";
-    private static final boolean IS_VERBOSE =
-            FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.USEREVENT);
+    private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.USEREVENT);
     private static final String UUID_STORAGE = "uuid";
 
-    public static UserEventDispatcher newInstance(Context context,
-            UserEventDelegate delegate) {
+    /**
+     * A factory method for UserEventDispatcher
+     */
+    public static UserEventDispatcher newInstance(Context context) {
         SharedPreferences sharedPrefs = Utilities.getDevicePrefs(context);
         String uuidStr = sharedPrefs.getString(UUID_STORAGE, null);
         if (uuidStr == null) {
@@ -81,41 +90,31 @@
         }
         UserEventDispatcher ued = Overrides.getObject(UserEventDispatcher.class,
                 context.getApplicationContext(), R.string.user_event_dispatcher_class);
-        ued.mDelegate = delegate;
         ued.mUuidStr = uuidStr;
         ued.mInstantAppResolver = InstantAppResolver.newInstance(context);
         return ued;
     }
 
-    public static UserEventDispatcher newInstance(Context context) {
-        return newInstance(context, null);
-    }
-
-    public interface UserEventDelegate {
-        void modifyUserEvent(LauncherEvent event);
-    }
 
     /**
      * Fills in the container data on the given event if the given view is not null.
      *
      * @return whether container data was added.
      */
-    public boolean fillInLogContainerData(LauncherLogProto.LauncherEvent event, @Nullable View v) {
-        // Fill in grid(x,y), pageIndex of the child and container type of the parent
-        LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(v);
-        if (v == null || !(v.getTag() instanceof ItemInfo) || provider == null) {
+    public boolean fillLogContainer(@Nullable View v, Target child,
+            @Nullable ArrayList<Target> targets) {
+        LogContainerProvider firstParent = StatsLogUtils.getLaunchProviderRecursive(v);
+        if (v == null || !(v.getTag() instanceof ItemInfo) || firstParent == null) {
             return false;
         }
         final ItemInfo itemInfo = (ItemInfo) v.getTag();
-        final Target target = event.srcTarget[0];
-        final Target targetParent = event.srcTarget[1];
-        onFillInLogContainerData(itemInfo, target, targetParent);
-        provider.fillInLogContainerData(v, itemInfo, target, targetParent);
+        firstParent.fillInLogContainerData(itemInfo, child, targets);
         return true;
     }
 
-    protected void onFillInLogContainerData(
-            @NonNull ItemInfo itemInfo, @NonNull Target target, @NonNull Target targetParent) { }
+    protected void onFillInLogContainerData(@NonNull ItemInfo itemInfo, @NonNull Target target,
+            @NonNull ArrayList<Target> targets) {
+    }
 
     private boolean mSessionStarted;
     private long mElapsedContainerMillis;
@@ -124,7 +123,6 @@
     private String mUuidStr;
     protected InstantAppResolver mInstantAppResolver;
     private boolean mAppOrTaskLaunch;
-    private UserEventDelegate mDelegate;
     private boolean mPreviousHomeGesture;
 
     //                      APP_ICON    SHORTCUT    WIDGET
@@ -135,16 +133,15 @@
     // --------------------------------------------------------------
 
     @Deprecated
-    public void logAppLaunch(View v, Intent intent) {
-        LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
-                newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
-
-        if (fillInLogContainerData(event, v)) {
-            if (mDelegate != null) {
-                mDelegate.modifyUserEvent(event);
-            }
-            fillIntentInfo(event.srcTarget[0], intent);
+    public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
+        Target itemTarget = newItemTarget(v, mInstantAppResolver);
+        Action action = newTouchAction(Action.Touch.TAP);
+        ArrayList<Target> targets = makeTargetsList(itemTarget);
+        if (fillLogContainer(v, itemTarget, targets)) {
+            onFillInLogContainerData((ItemInfo) v.getTag(), itemTarget, targets);
+            fillIntentInfo(itemTarget, intent, userHandle);
         }
+        LauncherEvent event = newLauncherEvent(action,  targets);
         dispatchUserEvent(event, intent);
         mAppOrTaskLaunch = true;
     }
@@ -164,15 +161,16 @@
             // Direction DOWN means the task was launched, UP means it was dismissed.
             event.action.dir = direction;
         }
-        event.srcTarget[0].itemType = LauncherLogProto.ItemType.TASK;
+        event.srcTarget[0].itemType = ItemType.TASK;
         event.srcTarget[0].pageIndex = taskIndex;
         fillComponentInfo(event.srcTarget[0], componentKey.componentName);
         dispatchUserEvent(event, null);
         mAppOrTaskLaunch = true;
     }
 
-    protected void fillIntentInfo(Target target, Intent intent) {
+    protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) {
         target.intentHash = intent.hashCode();
+        target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle());
         fillComponentInfo(target, intent.getComponent());
     }
 
@@ -186,8 +184,11 @@
     public void logNotificationLaunch(View v, PendingIntent intent) {
         LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
                 newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
-        if (fillInLogContainerData(event, v)) {
-            event.srcTarget[0].packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode();
+        Target itemTarget = newItemTarget(v, mInstantAppResolver);
+        ArrayList<Target> targets = makeTargetsList(itemTarget);
+
+        if (fillLogContainer(v, itemTarget, targets)) {
+            itemTarget.packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode();
         }
         dispatchUserEvent(event, null);
     }
@@ -233,50 +234,45 @@
         LauncherEvent event = newLauncherEvent(newCommandAction(command),
                 newItemTarget(itemView, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
 
-        if (fillInLogContainerData(event, itemView)) {
+        Target itemTarget = newItemTarget(itemView, mInstantAppResolver);
+        ArrayList<Target> targets = makeTargetsList(itemTarget);
+
+        if (fillLogContainer(itemView, itemTarget, targets)) {
             // TODO: Remove the following two lines once fillInLogContainerData can take in a
             // container view.
-            event.srcTarget[0].type = Target.Type.CONTAINER;
-            event.srcTarget[0].containerType = srcContainerType;
+            itemTarget.type = Target.Type.CONTAINER;
+            itemTarget.containerType = srcContainerType;
         }
         dispatchUserEvent(event, null);
     }
 
     public void logActionOnControl(int action, int controlType) {
-        logActionOnControl(action, controlType, null, -1);
+        logActionOnControl(action, controlType, null);
     }
 
     public void logActionOnControl(int action, int controlType, int parentContainerType) {
         logActionOnControl(action, controlType, null, parentContainerType);
     }
 
-    public void logActionOnControl(int action, int controlType, @Nullable View controlInContainer) {
-        logActionOnControl(action, controlType, controlInContainer, -1);
-    }
+    /**
+     * Logs control action with proper parent hierarchy
+     */
+    public void logActionOnControl(int actionType, int controlType,
+            @Nullable View controlInContainer, int... parentTypes) {
+        Target control = newTarget(Target.Type.CONTROL);
+        control.controlType = controlType;
+        Action action = newAction(actionType);
 
-    public void logActionOnControl(int action, int controlType, int parentContainer,
-            int grandParentContainer) {
-        LauncherEvent event = newLauncherEvent(newTouchAction(action),
-                newControlTarget(controlType),
-                newContainerTarget(parentContainer),
-                newContainerTarget(grandParentContainer));
-        dispatchUserEvent(event, null);
-    }
-
-    public void logActionOnControl(int action, int controlType, @Nullable View controlInContainer,
-            int parentContainerType) {
-        final LauncherEvent event = (controlInContainer == null && parentContainerType < 0)
-                ? newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL))
-                : newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL),
-                newTarget(Target.Type.CONTAINER));
-        event.srcTarget[0].controlType = controlType;
+        ArrayList<Target> targets = makeTargetsList(control);
         if (controlInContainer != null) {
-            fillInLogContainerData(event, controlInContainer);
+            fillLogContainer(controlInContainer, control, targets);
         }
-        if (parentContainerType >= 0) {
-            event.srcTarget[1].containerType = parentContainerType;
+        for (int parentContainerType : parentTypes) {
+            if (parentContainerType < 0) continue;
+            targets.add(newContainerTarget(parentContainerType));
         }
-        if (action == Action.Touch.DRAGDROP) {
+        LauncherEvent event = newLauncherEvent(action, targets);
+        if (actionType == Action.Touch.DRAGDROP) {
             event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis;
         }
         dispatchUserEvent(event, null);
@@ -292,7 +288,7 @@
     public void logActionBounceTip(int containerType) {
         LauncherEvent event = newLauncherEvent(newAction(Action.Type.TIP),
                 newContainerTarget(containerType));
-        event.srcTarget[0].tipType = LauncherLogProto.TipType.BOUNCE;
+        event.srcTarget[0].tipType = TipType.BOUNCE;
         dispatchUserEvent(event, null);
     }
 
@@ -319,7 +315,7 @@
             int srcChildTargetType, int srcParentContainerType, int dstContainerType,
             int pageIndex) {
         LauncherEvent event;
-        if (srcChildTargetType == LauncherLogProto.ItemType.TASK) {
+        if (srcChildTargetType == ItemType.TASK) {
             event = newLauncherEvent(newTouchAction(action),
                     newItemTarget(srcChildTargetType),
                     newContainerTarget(srcParentContainerType));
@@ -363,37 +359,57 @@
         dispatchUserEvent(event, null);
     }
 
-    public void logDeepShortcutsOpen(View icon) {
-        LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(icon);
-        if (icon == null || !(icon.getTag() instanceof ItemInfo || provider == null)) {
-            return;
-        }
-        ItemInfo info = (ItemInfo) icon.getTag();
-        LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.LONGPRESS),
-                newItemTarget(info, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
-        provider.fillInLogContainerData(icon, info, event.srcTarget[0], event.srcTarget[1]);
-        dispatchUserEvent(event, null);
+    /**
+     * Logs proto lite version of LauncherEvent object to clearcut.
+     */
+    public void logLauncherEvent(
+            com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent) {
 
+        if (mPreviousHomeGesture) {
+            mPreviousHomeGesture = false;
+        }
+        mAppOrTaskLaunch = false;
+        launcherEvent.toBuilder()
+                .setElapsedContainerMillis(SystemClock.uptimeMillis() - mElapsedContainerMillis)
+                .setElapsedSessionMillis(
+                        SystemClock.uptimeMillis() - mElapsedSessionMillis).build();
+        try {
+            dispatchUserEvent(LauncherEvent.parseFrom(launcherEvent.toByteArray()), null);
+        } catch (InvalidProtocolBufferNanoException e) {
+            throw new RuntimeException("Cannot convert LauncherEvent from Lite to Nano version.");
+        }
+    }
+
+    public void logDeepShortcutsOpen(View icon) {
+        ItemInfo info = (ItemInfo) icon.getTag();
+        Target child = newItemTarget(info, mInstantAppResolver);
+        ArrayList<Target> targets = makeTargetsList(child);
+        fillLogContainer(icon, child, targets);
+        dispatchUserEvent(newLauncherEvent(newTouchAction(Action.Touch.TAP), targets), null);
         resetElapsedContainerMillis("deep shortcut open");
     }
 
     public void logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView) {
-        LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP),
-                newItemTarget(dragObj.originalDragInfo, mInstantAppResolver),
-                newTarget(Target.Type.CONTAINER));
-        event.destTarget = new Target[]{
-                newItemTarget(dragObj.originalDragInfo, mInstantAppResolver),
-                newDropTarget(dropTargetAsView)
-        };
+        Target srcChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver);
+        ArrayList<Target> srcTargets = makeTargetsList(srcChild);
 
-        dragObj.dragSource.fillInLogContainerData(null, dragObj.originalDragInfo,
-                event.srcTarget[0], event.srcTarget[1]);
 
+        Target destChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver);
+        ArrayList<Target> destTargets = makeTargetsList(destChild);
+
+        dragObj.dragSource.fillInLogContainerData(dragObj.originalDragInfo, srcChild, srcTargets);
         if (dropTargetAsView instanceof LogContainerProvider) {
-            ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(null,
-                    dragObj.dragInfo, event.destTarget[0], event.destTarget[1]);
-
+            ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(dragObj.dragInfo,
+                    destChild, destTargets);
         }
+        else {
+            destTargets.add(newDropTarget(dropTargetAsView));
+        }
+        LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), srcTargets);
+        Target[] destTargetsArray = new Target[destTargets.size()];
+        destTargets.toArray(destTargetsArray);
+        event.destTarget = destTargetsArray;
+
         event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis;
         dispatchUserEvent(event, null);
     }
@@ -405,8 +421,8 @@
         action.command = Action.Command.BACK;
         action.dir = isButton ? Action.Direction.NONE :
                 gestureSwipeLeft ? Action.Direction.LEFT : Action.Direction.RIGHT;
-        Target target = newControlTarget(isButton ? LauncherLogProto.ControlType.BACK_BUTTON :
-                LauncherLogProto.ControlType.BACK_GESTURE);
+        Target target = newControlTarget(isButton ? ControlType.BACK_BUTTON :
+                ControlType.BACK_GESTURE);
         target.spanX = downX;
         target.spanY = downY;
         target.cardinality = completed ? 1 : 0;
@@ -417,8 +433,6 @@
 
     /**
      * Currently logs following containers: workspace, allapps, widget tray.
-     *
-     * @param reason
      */
     public final void resetElapsedContainerMillis(String reason) {
         mElapsedContainerMillis = SystemClock.uptimeMillis();
@@ -457,34 +471,23 @@
         if (!IS_VERBOSE) {
             return;
         }
-        Log.d(TAG, generateLog(ev));
+        LauncherLogProto.LauncherEvent liteLauncherEvent;
+        try {
+            liteLauncherEvent =
+                    LauncherLogProto.LauncherEvent.parseFrom(MessageNano.toByteArray(ev));
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException("Cannot parse LauncherEvent from Nano to Lite version");
+        }
+        Log.d(TAG, liteLauncherEvent.toString());
     }
 
     /**
-     * Returns a human-readable log for given user event.
+     * Constructs an ArrayList with targets
      */
-    public static String generateLog(LauncherEvent ev) {
-        String log = "\n-----------------------------------------------------"
-                + "\naction:" + LoggerUtils.getActionStr(ev.action);
-        if (ev.srcTarget != null && ev.srcTarget.length > 0) {
-            log += "\n Source " + getTargetsStr(ev.srcTarget);
-        }
-        if (ev.destTarget != null && ev.destTarget.length > 0) {
-            log += "\n Destination " + getTargetsStr(ev.destTarget);
-        }
-        log += String.format(Locale.US,
-                "\n Elapsed container %d ms, session %d ms, action %d ms",
-                ev.elapsedContainerMillis,
-                ev.elapsedSessionMillis,
-                ev.actionDurationMillis);
-        log += "\n\n";
-        return log;
-    }
-
-    private static String getTargetsStr(Target[] targets) {
-        String result = "child:" + LoggerUtils.getTargetStr(targets[0]);
-        for (int i = 1; i < targets.length; i++) {
-            result += "\tparent:" + LoggerUtils.getTargetStr(targets[i]);
+    public static ArrayList<Target> makeTargetsList(Target... targets) {
+        ArrayList<Target> result = new ArrayList<>();
+        for (Target target : targets) {
+            result.add(target);
         }
         return result;
     }
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index b8c583c..c236fa6 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -17,23 +17,23 @@
 
 import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.os.UserHandle;
 import android.util.LongSparseArray;
 import android.util.Pair;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+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.pm.InstallSessionHelper;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -93,9 +93,9 @@
                 }
             }
 
-            PackageInstallerCompat packageInstaller =
-                    PackageInstallerCompat.getInstance(app.getContext());
-            LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(app.getContext());
+            InstallSessionHelper packageInstaller =
+                    InstallSessionHelper.INSTANCE.get(app.getContext());
+            LauncherApps launcherApps = app.getContext().getSystemService(LauncherApps.class);
 
             for (ItemInfo item : filteredItems) {
                 // Find appropriate space for the item.
@@ -151,7 +151,7 @@
 
                         WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
                         wii.title = "";
-                        wii.applyFrom(app.getIconCache().getDefaultIcon(item.user));
+                        wii.bitmap = app.getIconCache().getDefaultIcon(item.user);
                         app.getIconCache().getTitleAndIcon(wii,
                                 ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
                     }
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 3873a17..eb5d106 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -16,28 +16,32 @@
 
 package com.android.launcher3.model;
 
-import static com.android.launcher3.AppInfo.COMPONENT_KEY_COMPARATOR;
-import static com.android.launcher3.AppInfo.EMPTY_ARRAY;
+import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
+import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.os.LocaleList;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AppFilter;
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.PromiseAppInfo;
+import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SafeCloseable;
 
 import java.util.ArrayList;
@@ -46,9 +50,6 @@
 import java.util.List;
 import java.util.function.Consumer;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 
 /**
  * Stores the list of all applications for the all apps view.
@@ -73,6 +74,13 @@
     private AlphabeticIndexCompat mIndex;
 
     /**
+     * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
+     * @see Callbacks#FLAG_QUIET_MODE_ENABLED
+     * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
+     */
+    private int mFlags;
+
+    /**
      * Boring constructor.
      */
     public AllAppsList(IconCache iconCache, AppFilter appFilter) {
@@ -91,6 +99,33 @@
     }
 
     /**
+     * Helper to checking {@link Callbacks#FLAG_HAS_SHORTCUT_PERMISSION}
+     */
+    public boolean hasShortcutHostPermission() {
+        return (mFlags & Callbacks.FLAG_HAS_SHORTCUT_PERMISSION) != 0;
+    }
+
+    /**
+     * Sets or clears the provided flag
+     */
+    public void setFlags(int flagMask, boolean enabled) {
+        if (enabled) {
+            mFlags |= flagMask;
+        } else {
+            mFlags &= ~flagMask;
+        }
+        mDataChanged = true;
+    }
+
+    /**
+     * Returns the model flags
+     */
+    public int getFlags() {
+        return mFlags;
+    }
+
+
+    /**
      * Add the supplied ApplicationInfo objects to the list, and enqueue it into the
      * list to broadcast when notify() is called.
      *
@@ -110,10 +145,9 @@
         mDataChanged = true;
     }
 
-    public void addPromiseApp(Context context,
-                              PackageInstallerCompat.PackageInstallInfo installInfo) {
-        ApplicationInfo applicationInfo = LauncherAppsCompat.getInstance(context)
-                .getApplicationInfo(installInfo.packageName, 0, installInfo.user);
+    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) {
             PromiseAppInfo info = new PromiseAppInfo(installInfo);
@@ -134,10 +168,10 @@
                     && appInfo.user.equals(user)
                     && appInfo instanceof PromiseAppInfo) {
                 final PromiseAppInfo promiseAppInfo = (PromiseAppInfo) appInfo;
-                if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLING) {
+                if (installInfo.state == PackageInstallInfo.STATUS_INSTALLING) {
                     promiseAppInfo.level = installInfo.progress;
                     return promiseAppInfo;
-                } else if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) {
+                } else if (installInfo.state == PackageInstallInfo.STATUS_FAILED) {
                     removeApp(i);
                 }
             }
@@ -164,11 +198,8 @@
      * Add the icons for the supplied apk called packageName.
      */
     public void addPackage(Context context, String packageName, UserHandle user) {
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
-        final List<LauncherActivityInfo> matches = launcherApps.getActivityList(packageName,
-                user);
-
-        for (LauncherActivityInfo info : matches) {
+        for (LauncherActivityInfo info : context.getSystemService(LauncherApps.class)
+                .getActivityList(packageName, user)) {
             add(new AppInfo(context, info, user), info);
         }
     }
@@ -214,9 +245,8 @@
      * Add and remove icons for this package which has been updated.
      */
     public void updatePackage(Context context, String packageName, UserHandle user) {
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
-        final List<LauncherActivityInfo> matches = launcherApps.getActivityList(packageName,
-                user);
+        final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
+                .getActivityList(packageName, user);
         if (matches.size() > 0) {
             // Find disabled/removed activities and remove them from data and add them
             // to the removed list.
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
index 13ab033..629a0ee 100644
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ b/src/com/android/launcher3/model/AppLaunchTracker.java
@@ -40,6 +40,7 @@
     public static final String CONTAINER_ALL_APPS = Integer.toString(ContainerType.ALLAPPS);
     public static final String CONTAINER_PREDICTIONS = Integer.toString(ContainerType.PREDICTION);
     public static final String CONTAINER_SEARCH = Integer.toString(ContainerType.SEARCHRESULT);
+    public static final String CONTAINER_OVERVIEW = Integer.toString(ContainerType.OVERVIEW);
 
 
     public static final MainThreadInitializedObject<AppLaunchTracker> INSTANCE =
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 0a4f005..8b0ef7b 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -16,31 +16,28 @@
 
 package com.android.launcher3.model;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
+import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
 
-import android.os.Looper;
 import android.util.Log;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.PagedView;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+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.util.IntArray;
-import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -52,40 +49,29 @@
     protected static final int INVALID_SCREEN_ID = -1;
     private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
 
-    protected final Executor mUiExecutor;
+    protected final LooperExecutor mUiExecutor;
 
     protected final LauncherAppState mApp;
     protected final BgDataModel mBgDataModel;
     private final AllAppsList mBgAllAppsList;
-    protected final int mPageToBindFirst;
 
-    protected final WeakReference<Callbacks> mCallbacks;
+    private final Callbacks[] mCallbacksList;
 
     private int mMyBindingId;
 
     public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        mUiExecutor = MAIN_EXECUTOR;
+            AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) {
+        mUiExecutor = uiExecutor;
         mApp = app;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
-        mPageToBindFirst = pageToBindFirst;
-        mCallbacks = callbacks == null ? new WeakReference<>(null) : callbacks;
+        mCallbacksList = callbacksList;
     }
 
     /**
      * Binds all loaded data to actual views on the main thread.
      */
     public void bindWorkspace() {
-        Callbacks callbacks = mCallbacks.get();
-        // Don't use these two variables in any of the callback runnables.
-        // Otherwise we hold a reference to them.
-        if (callbacks == null) {
-            // This launcher has exited and nobody bothered to tell us.  Just bail.
-            Log.w(TAG, "LoaderTask running with no launcher");
-            return;
-        }
-
         // Save a copy of all the bg-thread collections
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -99,179 +85,9 @@
             mMyBindingId = mBgDataModel.lastBindId;
         }
 
-        final int currentScreen;
-        {
-            int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE
-                    ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen();
-            if (currScreen >= orderedScreenIds.size()) {
-                // There may be no workspace screens (just hotseat items and an empty page).
-                currScreen = PagedView.INVALID_RESTORE_PAGE;
-            }
-            currentScreen = currScreen;
-        }
-        final boolean validFirstPage = currentScreen >= 0;
-        final int currentScreenId =
-                validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
-
-        // Separate the items that are on the current screen, and all the other remaining items
-        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
-        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
-        filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
-                otherWorkspaceItems);
-        filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
-                otherAppWidgets);
-        sortWorkspaceItemsSpatially(currentWorkspaceItems);
-        sortWorkspaceItemsSpatially(otherWorkspaceItems);
-
-        // Tell the workspace that we're about to start binding items
-        executeCallbacksTask(c -> {
-            c.clearPendingBinds();
-            c.startBinding();
-        }, mUiExecutor);
-
-        // Bind workspace screens
-        executeCallbacksTask(c -> c.bindScreens(orderedScreenIds), mUiExecutor);
-
-        Executor mainExecutor = mUiExecutor;
-        // Load items on the current page.
-        bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
-        bindAppWidgets(currentAppWidgets, mainExecutor);
-        // In case of validFirstPage, only bind the first screen, and defer binding the
-        // remaining screens after first onDraw (and an optional the fade animation whichever
-        // happens later).
-        // This ensures that the first screen is immediately visible (eg. during rotation)
-        // In case of !validFirstPage, bind all pages one after other.
-        final Executor deferredExecutor =
-                validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
-        executeCallbacksTask(c -> c.finishFirstPageBind(
-                validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
-        bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
-        bindAppWidgets(otherAppWidgets, deferredExecutor);
-        // Tell the workspace that we're done binding items
-        executeCallbacksTask(c -> c.finishBindingItems(mPageToBindFirst), deferredExecutor);
-
-        if (validFirstPage) {
-            executeCallbacksTask(c -> {
-                // We are loading synchronously, which means, some of the pages will be
-                // bound after first draw. Inform the callbacks that page binding is
-                // not complete, and schedule the remaining pages.
-                if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
-                    c.onPageBoundSynchronously(currentScreen);
-                }
-                c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
-            }, mUiExecutor);
-        }
-    }
-
-
-    /** Filters the set of items who are directly or indirectly (via another container) on the
-     * specified screen. */
-    public static <T extends ItemInfo> void filterCurrentWorkspaceItems(int currentScreenId,
-            ArrayList<T> allWorkspaceItems,
-            ArrayList<T> currentScreenItems,
-            ArrayList<T> otherScreenItems) {
-        // Purge any null ItemInfos
-        Iterator<T> iter = allWorkspaceItems.iterator();
-        while (iter.hasNext()) {
-            ItemInfo i = iter.next();
-            if (i == null) {
-                iter.remove();
-            }
-        }
-
-        // Order the set of items by their containers first, this allows use to walk through the
-        // list sequentially, build up a list of containers that are in the specified screen,
-        // as well as all items in those containers.
-        IntSet itemsOnScreen = new IntSet();
-        Collections.sort(allWorkspaceItems,
-                (lhs, rhs) -> Integer.compare(lhs.container, rhs.container));
-
-        for (T info : allWorkspaceItems) {
-            if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                if (info.screenId == currentScreenId) {
-                    currentScreenItems.add(info);
-                    itemsOnScreen.add(info.id);
-                } else {
-                    otherScreenItems.add(info);
-                }
-            } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                currentScreenItems.add(info);
-                itemsOnScreen.add(info.id);
-            } else {
-                if (itemsOnScreen.contains(info.container)) {
-                    currentScreenItems.add(info);
-                    itemsOnScreen.add(info.id);
-                } else {
-                    otherScreenItems.add(info);
-                }
-            }
-        }
-    }
-
-    /** Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to
-     * right) */
-    protected void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) {
-        final InvariantDeviceProfile profile = mApp.getInvariantDeviceProfile();
-        final int screenCols = profile.numColumns;
-        final int screenCellCount = profile.numColumns * profile.numRows;
-        Collections.sort(workspaceItems, new Comparator<ItemInfo>() {
-            @Override
-            public int compare(ItemInfo lhs, ItemInfo rhs) {
-                if (lhs.container == rhs.container) {
-                    // Within containers, order by their spatial position in that container
-                    switch (lhs.container) {
-                        case LauncherSettings.Favorites.CONTAINER_DESKTOP: {
-                            int lr = (lhs.screenId * screenCellCount +
-                                    lhs.cellY * screenCols + lhs.cellX);
-                            int rr = (rhs.screenId * screenCellCount +
-                                    rhs.cellY * screenCols + rhs.cellX);
-                            return Integer.compare(lr, rr);
-                        }
-                        case LauncherSettings.Favorites.CONTAINER_HOTSEAT: {
-                            // We currently use the screen id as the rank
-                            return Integer.compare(lhs.screenId, rhs.screenId);
-                        }
-                        default:
-                            if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                                throw new RuntimeException("Unexpected container type when " +
-                                        "sorting workspace items.");
-                            }
-                            return 0;
-                    }
-                } else {
-                    // Between containers, order by hotseat, desktop
-                    return Integer.compare(lhs.container, rhs.container);
-                }
-            }
-        });
-    }
-
-    protected void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
-            final Executor executor) {
-        // Bind the workspace items
-        int N = workspaceItems.size();
-        for (int i = 0; i < N; i += ITEMS_CHUNK) {
-            final int start = i;
-            final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
-            executeCallbacksTask(
-                    c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                    executor);
-        }
-    }
-
-    private void bindAppWidgets(ArrayList<LauncherAppWidgetInfo> appWidgets, Executor executor) {
-        int N;// Bind the widgets, one at a time
-        N = appWidgets.size();
-        for (int i = 0; i < N; i++) {
-            final ItemInfo widget = appWidgets.get(i);
-            executeCallbacksTask(
-                    c -> c.bindItems(Collections.singletonList(widget), false), executor);
+        for (Callbacks cb : mCallbacksList) {
+            new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+                    workspaceItems, appWidgets, orderedScreenIds).bind();
         }
     }
 
@@ -280,7 +96,8 @@
     public void bindAllApps() {
         // shallow copy
         AppInfo[] apps = mBgAllAppsList.copyData();
-        executeCallbacksTask(c -> c.bindAllApplications(apps), mUiExecutor);
+        int flags = mBgAllAppsList.getFlags();
+        executeCallbacksTask(c -> c.bindAllApplications(apps, flags), mUiExecutor);
     }
 
     public abstract void bindWidgets();
@@ -291,19 +108,164 @@
                 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
                 return;
             }
-            Callbacks callbacks = mCallbacks.get();
-            if (callbacks != null) {
-                task.execute(callbacks);
+            for (Callbacks cb : mCallbacksList) {
+                task.execute(cb);
             }
         });
     }
 
     public LooperIdleLock newIdleLock(Object lock) {
-        LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
-        // If we are not binding, there is no reason to wait for idle.
-        if (mCallbacks.get() == null) {
+        LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper());
+        // If we are not binding or if the main looper is already idle, there is no reason to wait
+        if (mUiExecutor.getLooper().getQueue().isIdle()) {
             idleLock.queueIdle();
         }
         return idleLock;
     }
+
+    private static class WorkspaceBinder {
+
+        private final Executor mUiExecutor;
+        private final Callbacks mCallbacks;
+
+        private final LauncherAppState mApp;
+        private final BgDataModel mBgDataModel;
+
+        private final int mMyBindingId;
+        private final ArrayList<ItemInfo> mWorkspaceItems;
+        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final IntArray mOrderedScreenIds;
+
+
+        WorkspaceBinder(Callbacks callbacks,
+                Executor uiExecutor,
+                LauncherAppState app,
+                BgDataModel bgDataModel,
+                int myBindingId,
+                ArrayList<ItemInfo> workspaceItems,
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                IntArray orderedScreenIds) {
+            mCallbacks = callbacks;
+            mUiExecutor = uiExecutor;
+            mApp = app;
+            mBgDataModel = bgDataModel;
+            mMyBindingId = myBindingId;
+            mWorkspaceItems = workspaceItems;
+            mAppWidgets = appWidgets;
+            mOrderedScreenIds = orderedScreenIds;
+        }
+
+        private void bind() {
+            final int currentScreen;
+            {
+                // Create an anonymous scope to calculate currentScreen as it has to be a
+                // final variable.
+                int currScreen = mCallbacks.getPageToBindSynchronously();
+                if (currScreen >= mOrderedScreenIds.size()) {
+                    // There may be no workspace screens (just hotseat items and an empty page).
+                    currScreen = PagedView.INVALID_PAGE;
+                }
+                currentScreen = currScreen;
+            }
+            final boolean validFirstPage = currentScreen >= 0;
+            final int currentScreenId =
+                    validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+            // Separate the items that are on the current screen, and all the other remaining items
+            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+            filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+                    otherWorkspaceItems);
+            filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+                    otherAppWidgets);
+            final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
+            sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
+            sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+
+            // Tell the workspace that we're about to start binding items
+            executeCallbacksTask(c -> {
+                c.clearPendingBinds();
+                c.startBinding();
+            }, mUiExecutor);
+
+            // Bind workspace screens
+            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+
+            Executor mainExecutor = mUiExecutor;
+            // Load items on the current page.
+            bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
+            bindAppWidgets(currentAppWidgets, mainExecutor);
+
+            // Locate available spots for prediction using currentWorkspaceItems
+            IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons);
+            bindPredictedItems(gaps, mainExecutor);
+            // In case of validFirstPage, only bind the first screen, and defer binding the
+            // remaining screens after first onDraw (and an optional the fade animation whichever
+            // happens later).
+            // This ensures that the first screen is immediately visible (eg. during rotation)
+            // In case of !validFirstPage, bind all pages one after other.
+            final Executor deferredExecutor =
+                    validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
+
+            executeCallbacksTask(c -> c.finishFirstPageBind(
+                    validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
+
+            bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
+            bindAppWidgets(otherAppWidgets, deferredExecutor);
+            // Tell the workspace that we're done binding items
+            executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
+
+            if (validFirstPage) {
+                executeCallbacksTask(c -> {
+                    // We are loading synchronously, which means, some of the pages will be
+                    // bound after first draw. Inform the mCallbacks that page binding is
+                    // not complete, and schedule the remaining pages.
+                    c.onPageBoundSynchronously(currentScreen);
+                    c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+
+                }, mUiExecutor);
+            }
+        }
+
+        private void bindWorkspaceItems(
+                final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+            // Bind the workspace items
+            int count = workspaceItems.size();
+            for (int i = 0; i < count; i += ITEMS_CHUNK) {
+                final int start = i;
+                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+                executeCallbacksTask(
+                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
+                        executor);
+            }
+        }
+
+        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
+            // Bind the widgets, one at a time
+            int count = appWidgets.size();
+            for (int i = 0; i < count; i++) {
+                final ItemInfo widget = appWidgets.get(i);
+                executeCallbacksTask(
+                        c -> c.bindItems(Collections.singletonList(widget), false), executor);
+            }
+        }
+
+        private void bindPredictedItems(IntArray ranks, final Executor executor) {
+            ArrayList<AppInfo> items = new ArrayList<>(mBgDataModel.cachedPredictedItems);
+            executeCallbacksTask(c -> c.bindPredictedItems(items, ranks), executor);
+        }
+
+        protected void executeCallbacksTask(CallbackTask task, Executor executor) {
+            executor.execute(() -> {
+                if (mMyBindingId != mBgDataModel.lastBindId) {
+                    Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
+                    return;
+                }
+                task.execute(mCallbacks);
+            });
+        }
+    }
 }
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index e12633b..9013cba 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -17,20 +17,19 @@
 
 import android.util.Log;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.data.AppInfo;
+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 java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -78,13 +77,9 @@
      * Schedules a {@param task} to be executed on the current callbacks.
      */
     public final void scheduleCallbackTask(final CallbackTask task) {
-        final Callbacks callbacks = mModel.getCallback();
-        mUiExecutor.execute(() -> {
-            Callbacks cb = mModel.getCallback();
-            if (callbacks == cb && cb != null) {
-                task.execute(callbacks);
-            }
-        });
+        for (final Callbacks cb : mModel.getCallbacks()) {
+            mUiExecutor.execute(() -> task.execute(cb));
+        }
     }
 
     public ModelWriter getModelWriter() {
@@ -122,7 +117,8 @@
     public void bindApplicationsIfNeeded() {
         if (mAllAppsList.getAndResetChangeFlag()) {
             AppInfo[] apps = mAllAppsList.copyData();
-            scheduleCallbackTask(c -> c.bindAllApplications(apps));
+            int flags = mAllAppsList.getFlags();
+            scheduleCallbackTask(c -> c.bindAllApplications(apps, flags));
         }
     }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 0e20270..9bef847 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,29 +15,29 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
+
 import android.content.Context;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.MutableInt;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InstallShortcutReceiver;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.PromiseAppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logging.DumpTargetWrapper;
-import com.android.launcher3.model.nano.LauncherDumpProto;
-import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
-import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.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.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -46,11 +46,7 @@
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
-import com.google.protobuf.nano.MessageNano;
-
 import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -59,6 +55,8 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 
 /**
  * All the data stored in-memory and managed by the LauncherModel
@@ -95,9 +93,16 @@
     public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
 
     /**
-     * True if the launcher has permission to access deep shortcuts.
+     * List of all cached predicted items visible on home screen
      */
-    public boolean hasShortcutHostPermission;
+    public final ArrayList<AppInfo> cachedPredictedItems = new ArrayList<>();
+
+    /**
+     * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
+     * @see Callbacks#FLAG_QUIET_MODE_ENABLED
+     * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
+     */
+    public int flags;
 
     /**
      * Maps all launcher activities to counts of their shortcuts.
@@ -144,10 +149,6 @@
 
     public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
             String[] args) {
-        if (Arrays.asList(args).contains("--proto")) {
-            dumpProto(prefix, fd, writer, args);
-            return;
-        }
         writer.println(prefix + "Data Model:");
         writer.println(prefix + " ---- workspace items ");
         for (int i = 0; i < workspaceItems.size(); i++) {
@@ -175,89 +176,6 @@
         }
     }
 
-    private synchronized void dumpProto(String prefix, FileDescriptor fd, PrintWriter writer,
-            String[] args) {
-
-        // Add top parent nodes. (L1)
-        DumpTargetWrapper hotseat = new DumpTargetWrapper(ContainerType.HOTSEAT, 0);
-        IntSparseArrayMap<DumpTargetWrapper> workspaces = new IntSparseArrayMap<>();
-        IntArray workspaceScreens = collectWorkspaceScreens();
-        for (int i = 0; i < workspaceScreens.size(); i++) {
-            workspaces.put(workspaceScreens.get(i),
-                    new DumpTargetWrapper(ContainerType.WORKSPACE, i));
-        }
-        DumpTargetWrapper dtw;
-        // Add non leaf / non top nodes (L2)
-        for (int i = 0; i < folders.size(); i++) {
-            FolderInfo fInfo = folders.valueAt(i);
-            dtw = new DumpTargetWrapper(ContainerType.FOLDER, folders.size());
-            dtw.writeToDumpTarget(fInfo);
-            for(WorkspaceItemInfo sInfo: fInfo.contents) {
-                DumpTargetWrapper child = new DumpTargetWrapper(sInfo);
-                child.writeToDumpTarget(sInfo);
-                dtw.add(child);
-            }
-            if (fInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                hotseat.add(dtw);
-            } else if (fInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                workspaces.get(fInfo.screenId).add(dtw);
-            }
-        }
-        // Add leaf nodes (L3): *Info
-        for (int i = 0; i < workspaceItems.size(); i++) {
-            ItemInfo info = workspaceItems.get(i);
-            if (info instanceof FolderInfo) {
-                continue;
-            }
-            dtw = new DumpTargetWrapper(info);
-            dtw.writeToDumpTarget(info);
-            if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                hotseat.add(dtw);
-            } else if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                workspaces.get(info.screenId).add(dtw);
-            }
-        }
-        for (int i = 0; i < appWidgets.size(); i++) {
-            ItemInfo info = appWidgets.get(i);
-            dtw = new DumpTargetWrapper(info);
-            dtw.writeToDumpTarget(info);
-            if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                hotseat.add(dtw);
-            } else if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                workspaces.get(info.screenId).add(dtw);
-            }
-        }
-
-
-        // Traverse target wrapper
-        ArrayList<DumpTarget> targetList = new ArrayList<>();
-        targetList.addAll(hotseat.getFlattenedList());
-        for (int i = 0; i < workspaces.size(); i++) {
-            targetList.addAll(workspaces.valueAt(i).getFlattenedList());
-        }
-
-        if (Arrays.asList(args).contains("--debug")) {
-            for (int i = 0; i < targetList.size(); i++) {
-                writer.println(prefix + DumpTargetWrapper.getDumpTargetStr(targetList.get(i)));
-            }
-            return;
-        } else {
-            LauncherDumpProto.LauncherImpression proto = new LauncherDumpProto.LauncherImpression();
-            proto.targets = new DumpTarget[targetList.size()];
-            for (int i = 0; i < targetList.size(); i++) {
-                proto.targets[i] = targetList.get(i);
-            }
-            FileOutputStream fos = new FileOutputStream(fd);
-            try {
-
-                fos.write(MessageNano.toByteArray(proto));
-                Log.d(TAG, MessageNano.toByteArray(proto).length + "Bytes");
-            } catch (IOException e) {
-                Log.e(TAG, "Exception writing dumpsys --proto", e);
-            }
-        }
-    }
-
     public synchronized void removeItem(Context context, ItemInfo... items) {
         removeItem(context, Arrays.asList(items));
     }
@@ -267,7 +185,7 @@
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                     folders.remove(item.id);
-                    if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    if (FeatureFlags.IS_STUDIO_BUILD) {
                         for (ItemInfo info : itemsIdMap) {
                             if (info.container == item.id) {
                                 // We are deleting a folder which still contains items that
@@ -287,7 +205,7 @@
                     if ((count == null || --count.value == 0)
                             && !InstallShortcutReceiver.getPendingShortcuts(context)
                                 .contains(pinnedShortcut)) {
-                        DeepShortcutManager.getInstance(context).unpinShortcut(pinnedShortcut);
+                        unpinShortcut(context, pinnedShortcut);
                     }
                     // Fall through.
                 }
@@ -324,7 +242,7 @@
 
                 // Since this is a new item, pin the shortcut in the system server.
                 if (newItem && count.value == 1) {
-                    DeepShortcutManager.getInstance(context).pinShortcut(pinnedShortcut);
+                    updatePinnedShortcuts(context, pinnedShortcut, List::add);
                 }
                 // Fall through
             }
@@ -355,6 +273,36 @@
     }
 
     /**
+     * Removes the given shortcut from the current list of pinned shortcuts.
+     * (Runs on background thread)
+     */
+    public void unpinShortcut(Context context, ShortcutKey key) {
+        updatePinnedShortcuts(context, key, List::remove);
+    }
+
+    private void updatePinnedShortcuts(Context context, ShortcutKey key,
+            BiConsumer<List<String>, String> idOp) {
+        if (GO_DISABLE_WIDGETS) {
+            return;
+        }
+        String packageName = key.componentName.getPackageName();
+        String id = key.getId();
+        UserHandle user = key.user;
+        List<String> pinnedIds = new ShortcutRequest(context, user)
+                .forPackage(packageName)
+                .query(PINNED)
+                .stream()
+                .map(ShortcutInfo::getId)
+                .collect(Collectors.toCollection(ArrayList::new));
+        idOp.accept(pinnedIds, id);
+        try {
+            context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.w(TAG, "Failed to pin shortcut", e);
+        }
+    }
+
+    /**
      * Return an existing FolderInfo object if we have encountered this ID previously,
      * or make a new one.
      */
@@ -388,7 +336,8 @@
         // Now add the new shortcuts to the map.
         for (ShortcutInfo shortcut : shortcuts) {
             boolean shouldShowInContainer = shortcut.isEnabled()
-                    && (shortcut.isDeclaredInManifest() || shortcut.isDynamic());
+                    && (shortcut.isDeclaredInManifest() || shortcut.isDynamic())
+                    && shortcut.getActivity() != null;
             if (shouldShowInContainer) {
                 ComponentKey targetComponent
                         = new ComponentKey(shortcut.getActivity(), shortcut.getUserHandle());
@@ -400,9 +349,17 @@
     }
 
     public interface Callbacks {
-        void rebindModel();
+        // If the launcher has permission to access deep shortcuts.
+        int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
+        // If quiet mode is enabled for any user
+        int FLAG_QUIET_MODE_ENABLED = 1 << 1;
+        // If launcher can change quiet mode
+        int FLAG_QUIET_MODE_CHANGE_PERMISSION = 1 << 2;
 
-        int getCurrentWorkspaceScreen();
+        /**
+         * Returns the page number to bind first, synchronously if possible or -1
+         */
+        int getPageToBindSynchronously();
         void clearPendingBinds();
         void startBinding();
         void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
@@ -422,6 +379,11 @@
         void executeOnNextDraw(ViewOnDrawExecutor executor);
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
 
-        void bindAllApplications(AppInfo[] apps);
+        void bindAllApplications(AppInfo[] apps, int flags);
+
+        /**
+         * Binds predicted appInfos at at available prediction slots.
+         */
+        void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks);
     }
 }
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
index c1c8be3..8e6b064 100644
--- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java
+++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
@@ -18,11 +18,11 @@
 import android.content.ComponentName;
 import android.os.UserHandle;
 
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 import java.util.ArrayList;
 import java.util.HashSet;
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java
index a0b7177..5112304 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcast.java
+++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java
@@ -15,16 +15,18 @@
  */
 package com.android.launcher3.model;
 
+import static android.os.Process.myUserHandle;
+
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.util.Log;
 
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
+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.util.MultiHashMap;
 import com.android.launcher3.util.PackageUserKey;
 
@@ -35,8 +37,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import static android.os.Process.myUserHandle;
-
 /**
  * Helper class to send broadcasts to package installers that have:
  * - Items on the first screen
diff --git a/src/com/android/launcher3/model/GridBackupTable.java b/src/com/android/launcher3/model/GridBackupTable.java
index 804a040..acfc339 100644
--- a/src/com/android/launcher3/model/GridBackupTable.java
+++ b/src/com/android/launcher3/model/GridBackupTable.java
@@ -27,9 +27,11 @@
 import android.os.Process;
 import android.util.Log;
 
+import androidx.annotation.IntDef;
+
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.pm.UserCache;
 
 /**
  * Helper class to backup and restore Favorites table into a separate table
@@ -45,6 +47,19 @@
     private static final String KEY_GRID_Y_SIZE = Favorites.SPANY;
     private static final String KEY_DB_VERSION = Favorites.RANK;
 
+    public static final int OPTION_REQUIRES_SANITIZATION = 1;
+
+    /** STATE_NOT_FOUND indicates backup doesn't exist in the db. */
+    private static final int STATE_NOT_FOUND = 0;
+    /**
+     *  STATE_RAW indicates the backup has not yet been sanitized. This implies it might still
+     *  posses app info that doesn't exist in the workspace and needed to be sanitized before
+     *  put into use.
+     */
+    private static final int STATE_RAW = 1;
+    /** STATE_SANITIZED indicates the backup has already been sanitized, thus can be used as-is. */
+    private static final int STATE_SANITIZED = 2;
+
     private final Context mContext;
     private final SQLiteDatabase mDb;
 
@@ -56,8 +71,11 @@
     private int mRestoredGridX;
     private int mRestoredGridY;
 
-    public GridBackupTable(Context context, SQLiteDatabase db,
-            int hotseatSize, int gridX, int gridY) {
+    @IntDef({STATE_NOT_FOUND, STATE_RAW, STATE_SANITIZED})
+    private @interface BackupState { }
+
+    public GridBackupTable(Context context, SQLiteDatabase db, int hotseatSize, int gridX,
+            int gridY) {
         mContext = context;
         mDb = db;
 
@@ -66,6 +84,10 @@
         mOldGridY = gridY;
     }
 
+    /**
+     * Create a backup from current workspace layout if one isn't created already (Note backup
+     * created this way is always sanitized). Otherwise restore from the backup instead.
+     */
     public boolean backupOrRestoreAsNeeded() {
         // Check if backup table exists
         if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
@@ -74,16 +96,28 @@
                 // No need to copy if empty DB was created.
                 return false;
             }
+            doBackup(UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                    Process.myUserHandle()), 0);
+            return false;
+        }
+        return restoreIfBackupExists(Favorites.TABLE_NAME);
+    }
 
-            copyTable(Favorites.TABLE_NAME, BACKUP_TABLE_NAME);
-            encodeDBProperties();
+    public boolean restoreToPreviewIfBackupExists() {
+        if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
             return false;
         }
 
-        if (!loadDbProperties()) {
+        return restoreIfBackupExists(Favorites.PREVIEW_TABLE_NAME);
+    }
+
+    private boolean restoreIfBackupExists(String toTableName) {
+        if (loadDBProperties() != STATE_SANITIZED) {
             return false;
         }
-        copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME);
+        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                Process.myUserHandle());
+        copyTable(mDb, BACKUP_TABLE_NAME, toTableName, userSerial);
         Log.d(TAG, "Backup table found");
         return true;
     }
@@ -93,43 +127,110 @@
         return mRestoredHotseatSize;
     }
 
-    private void copyTable(String from, String to) {
-        long userSerial = UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
+    /**
+     * Creates a new table and populates with copy of Favorites.TABLE_NAME
+     */
+    public void createCustomBackupTable(String tableName) {
+        long profileId = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
                 Process.myUserHandle());
-        dropTable(mDb, to);
-        Favorites.addTableToDb(mDb, userSerial, false, to);
-        mDb.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY);
+        copyTable(mDb, Favorites.TABLE_NAME, tableName, profileId);
+        encodeDBProperties(0);
     }
 
-    private void encodeDBProperties() {
+    /**
+     *
+     * Restores the contents of a custom table to Favorites.TABLE_NAME
+     */
+
+    public void restoreFromCustomBackupTable(String tableName, boolean dropAfterUse) {
+        if (!tableExists(mDb, tableName)) {
+            return;
+        }
+        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                Process.myUserHandle());
+        copyTable(mDb, tableName, Favorites.TABLE_NAME, userSerial);
+        if (dropAfterUse) {
+            dropTable(mDb, tableName);
+        }
+    }
+    /**
+     * Copy valid grid entries from one table to another.
+     */
+    private static void copyTable(SQLiteDatabase db, String from, String to, long userSerial) {
+        dropTable(db, to);
+        Favorites.addTableToDb(db, userSerial, false, to);
+        db.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY);
+    }
+
+    private void encodeDBProperties(int options) {
         ContentValues values = new ContentValues();
         values.put(Favorites._ID, ID_PROPERTY);
         values.put(KEY_DB_VERSION, mDb.getVersion());
         values.put(KEY_GRID_X_SIZE, mOldGridX);
         values.put(KEY_GRID_Y_SIZE, mOldGridY);
         values.put(KEY_HOTSEAT_SIZE, mOldHotseatSize);
+        values.put(Favorites.OPTIONS, options);
         mDb.insert(BACKUP_TABLE_NAME, null, values);
     }
 
-    private boolean loadDbProperties() {
+    /**
+     * Load DB properties from grid backup table.
+     */
+    public @BackupState int loadDBProperties() {
         try (Cursor c = mDb.query(BACKUP_TABLE_NAME, new String[] {
-                        KEY_DB_VERSION,     // 0
-                        KEY_GRID_X_SIZE,    // 1
-                        KEY_GRID_Y_SIZE,    // 2
-                        KEY_HOTSEAT_SIZE},  // 3
+                KEY_DB_VERSION,     // 0
+                KEY_GRID_X_SIZE,    // 1
+                KEY_GRID_Y_SIZE,    // 2
+                KEY_HOTSEAT_SIZE,   // 3
+                Favorites.OPTIONS}, // 4
                 "_id=" + ID_PROPERTY, null, null, null, null)) {
             if (!c.moveToNext()) {
                 Log.e(TAG, "Meta data not found in backup table");
-                return false;
+                return STATE_NOT_FOUND;
             }
-            if (mDb.getVersion() != c.getInt(0)) {
-                return false;
+            if (!validateDBVersion(mDb.getVersion(), c.getInt(0))) {
+                return STATE_NOT_FOUND;
             }
 
             mRestoredGridX = c.getInt(1);
             mRestoredGridY = c.getInt(2);
             mRestoredHotseatSize = c.getInt(3);
-            return true;
+            boolean isSanitized = (c.getInt(4) & OPTION_REQUIRES_SANITIZATION) == 0;
+            return isSanitized ? STATE_SANITIZED : STATE_RAW;
         }
     }
+
+    /**
+     * Restore workspace from raw backup if available.
+     */
+    public boolean restoreFromRawBackupIfAvailable(long oldProfileId) {
+        if (!tableExists(mDb, Favorites.BACKUP_TABLE_NAME)
+                || loadDBProperties() != STATE_RAW
+                || mOldHotseatSize != mRestoredHotseatSize
+                || mOldGridX != mRestoredGridX
+                || mOldGridY != mRestoredGridY) {
+            // skip restore if dimensions in backup table differs from current setup.
+            return false;
+        }
+        copyTable(mDb, Favorites.BACKUP_TABLE_NAME, Favorites.TABLE_NAME, oldProfileId);
+        Log.d(TAG, "Backup restored");
+        return true;
+    }
+
+    /**
+     * Performs a backup on the workspace layout.
+     */
+    public void doBackup(long profileId, int options) {
+        copyTable(mDb, Favorites.TABLE_NAME, Favorites.BACKUP_TABLE_NAME, profileId);
+        encodeDBProperties(options);
+    }
+
+    private static boolean validateDBVersion(int expected, int actual) {
+        if (expected != actual) {
+            Log.e(TAG, String.format("Launcher.db version mismatch, expecting %d but %d was found",
+                    expected, actual));
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index 783e908..e8a52bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -1,8 +1,11 @@
 package com.android.launcher3.model;
 
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
 import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
 import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.Utilities.parsePoint;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -14,11 +17,13 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
+import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
@@ -26,22 +31,20 @@
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
-import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.function.Consumer;
-
-import androidx.annotation.VisibleForTesting;
 
 /**
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
@@ -52,9 +55,6 @@
     private static final String TAG = "GridSizeMigrationTask";
     private static final boolean DEBUG = true;
 
-    private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
-    private static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
-
     // These are carefully selected weights for various item types (Math.random?), to allow for
     // the least absurd migration experience.
     private static final float WT_SHORTCUT = 1;
@@ -71,6 +71,7 @@
 
     private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>();
     private final HashSet<String> mValidPackages;
+    private final String mTableName;
 
     private final int mSrcX, mSrcY;
     private final int mTrgX, mTrgY;
@@ -80,10 +81,12 @@
     private final int mDestHotseatSize;
 
     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
-            HashSet<String> validPackages, Point sourceSize, Point targetSize) {
+            HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize,
+            Point targetSize) {
         mContext = context;
         mDb = db;
         mValidPackages = validPackages;
+        mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
 
         mSrcX = sourceSize.x;
         mSrcY = sourceSize.y;
@@ -99,10 +102,12 @@
     }
 
     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
-            HashSet<String> validPackages, int srcHotseatSize, int destHotseatSize) {
+            HashSet<String> validPackages, boolean usePreviewTable, int srcHotseatSize,
+            int destHotseatSize) {
         mContext = context;
         mDb = db;
         mValidPackages = validPackages;
+        mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
 
         mSrcHotseatSize = srcHotseatSize;
 
@@ -122,7 +127,7 @@
         // Update items
         int updateCount = mUpdateOperations.size();
         for (int i = 0; i < updateCount; i++) {
-            mDb.update(Favorites.TABLE_NAME, mUpdateOperations.valueAt(i),
+            mDb.update(mTableName, mUpdateOperations.valueAt(i),
                     "_id=" + mUpdateOperations.keyAt(i), null);
         }
 
@@ -130,8 +135,8 @@
             if (DEBUG) {
                 Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
             }
-            mDb.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
-                    Favorites._ID, mEntryToRemove), null);
+            mDb.delete(mTableName, Utilities.createDbSelectionQuery(Favorites._ID, mEntryToRemove),
+                    null);
         }
 
         return updateCount > 0 || !mEntryToRemove.isEmpty();
@@ -184,8 +189,8 @@
     }
 
     @VisibleForTesting
-    static IntArray getWorkspaceScreenIds(SQLiteDatabase db) {
-        return LauncherDbUtils.queryIntArray(db, Favorites.TABLE_NAME, Favorites.SCREEN,
+    static IntArray getWorkspaceScreenIds(SQLiteDatabase db, String tableName) {
+        return LauncherDbUtils.queryIntArray(db, tableName, Favorites.SCREEN,
                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP,
                 Favorites.SCREEN, Favorites.SCREEN);
     }
@@ -194,7 +199,7 @@
      * @return true if any DB change was made
      */
     protected boolean migrateWorkspace() throws Exception {
-        IntArray allScreens = getWorkspaceScreenIds(mDb);
+        IntArray allScreens = getWorkspaceScreenIds(mDb, mTableName);
         if (allScreens.isEmpty()) {
             throw new Exception("Unable to get workspace screens");
         }
@@ -246,12 +251,12 @@
     /**
      * Migrate a particular screen id.
      * Strategy:
-     *   1) For all possible combinations of row and column, pick the one which causes the least
-     *      data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
-     *   2) Maintain a list of all lost items before this screen, and add any new item lost from
-     *      this screen to that list as well.
-     *   3) If all those items from the above list can be placed on this screen, place them
-     *      (otherwise they are placed on a new screen).
+     *  1) For all possible combinations of row and column, pick the one which causes the least
+     *    data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
+     *  2) Maintain a list of all lost items before this screen, and add any new item lost from
+     *    this screen to that list as well.
+     *  3) If all those items from the above list can be placed on this screen, place them
+     *    (otherwise they are placed on a new screen).
      */
     protected void migrateScreen(int screenId) {
         // If we are migrating the first screen, do not touch the first row.
@@ -364,9 +369,9 @@
     /**
      * Tries the remove the provided row and column.
      *
-     * @param items all the items on the screen under operation
+     * @param items   all the items on the screen under operation
      * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
-     * with the overall item movement.
+     *                with the overall item movement.
      */
     private ArrayList<DbEntry> tryRemove(int col, int row, int startY,
             ArrayList<DbEntry> items, float[] outLoss) {
@@ -381,13 +386,13 @@
 
         for (DbEntry item : items) {
             if ((item.cellX <= col && (item.spanX + item.cellX) > col)
-                || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
+                    || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
                 removedItems.add(item);
-                if (item.cellX >= col) item.cellX --;
-                if (item.cellY >= row) item.cellY --;
+                if (item.cellX >= col) item.cellX--;
+                if (item.cellY >= row) item.cellY--;
             } else {
-                if (item.cellX > col) item.cellX --;
-                if (item.cellY > row) item.cellY --;
+                if (item.cellX > col) item.cellX--;
+                if (item.cellY > row) item.cellY--;
                 finalItems.add(item);
                 occupied.markCells(item, true);
             }
@@ -440,9 +445,9 @@
         /**
          * Recursively finds a placement for the provided items.
          *
-         * @param index the position in {@link #itemsToPlace} to start looking at.
-         * @param weightLoss total weight loss upto this point
-         * @param moveCost total move cost upto this point
+         * @param index       the position in {@link #itemsToPlace} to start looking at.
+         * @param weightLoss  total weight loss upto this point
+         * @param moveCost    total move cost upto this point
          * @param itemsPlaced all the items already placed upto this point
          */
         public void find(int index, float weightLoss, float moveCost,
@@ -483,11 +488,11 @@
                         float newMoveCost = moveCost;
                         if (x != myX) {
                             me.cellX = x;
-                            newMoveCost ++;
+                            newMoveCost++;
                         }
                         if (y != myY) {
                             me.cellY = y;
-                            newMoveCost ++;
+                            newMoveCost++;
                         }
                         if (ignoreMove) {
                             newMoveCost = moveCost;
@@ -502,35 +507,35 @@
 
                         // Try resizing horizontally
                         if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
-                            me.spanX --;
+                            me.spanX--;
                             occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanX ++;
+                            me.spanX++;
                         }
 
                         // Try resizing vertically
                         if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
-                            me.spanY --;
+                            me.spanY--;
                             occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanY ++;
+                            me.spanY++;
                         }
 
                         // Try resizing horizontally & vertically
                         if (myH > me.minSpanY && myW > me.minSpanX &&
                                 occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
-                            me.spanX --;
-                            me.spanY --;
+                            me.spanX--;
+                            me.spanY--;
                             occupied.markCells(me, true);
                             // 2 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanX ++;
-                            me.spanY ++;
+                            me.spanX++;
+                            me.spanY++;
                         }
                         me.cellX = myX;
                         me.cellY = myY;
@@ -567,11 +572,11 @@
                     float newMoveCost = moveCost;
                     if (newX != myX) {
                         me.cellX = newX;
-                        newMoveCost ++;
+                        newMoveCost++;
                     }
                     if (newY != myY) {
                         me.cellY = newY;
-                        newMoveCost ++;
+                        newMoveCost++;
                     }
                     if (ignoreMove) {
                         newMoveCost = moveCost;
@@ -604,7 +609,7 @@
     }
 
     private ArrayList<DbEntry> loadHotseatEntries() {
-        Cursor c =  queryWorkspace(
+        Cursor c = queryWorkspace(
                 new String[]{
                         Favorites._ID,                  // 0
                         Favorites.ITEM_TYPE,            // 1
@@ -694,6 +699,7 @@
         final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
 
         ArrayList<DbEntry> entries = new ArrayList<>();
+        WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
         while (c.moveToNext()) {
             DbEntry entry = new DbEntry();
             entry.id = c.getInt(indexId);
@@ -723,8 +729,8 @@
                                 * entry.spanX * entry.spanY);
 
                         int widgetId = c.getInt(indexAppWidgetId);
-                        LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance(
-                                mContext).getLauncherAppWidgetInfo(widgetId);
+                        LauncherAppWidgetProviderInfo pInfo =
+                                widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
                         Point spans = null;
                         if (pInfo != null) {
                             spans = pInfo.getMinSpans();
@@ -788,7 +794,7 @@
     }
 
     protected Cursor queryWorkspace(String[] columns, String where) {
-        return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
+        return mDb.query(mTableName, columns, where, null, null, null, null);
     }
 
     /**
@@ -880,24 +886,43 @@
     }
 
     /**
-     * Migrates the workspace and hotseat in case their sizes changed.
+     * Check given a new IDP, if migration is necessary.
+     */
+    public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+        return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
+    }
+
+    /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
+    public static boolean migrateGridIfNeeded(Context context) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return true;
+        }
+        return migrateGridIfNeeded(context, null);
+    }
+
+    /**
+     * Run the migration algorithm if needed. For preview, we provide the intended idp because it
+     * has not been changed. If idp is null, we read it from the context, for actual grid migration.
      *
      * @return false if the migration failed.
      */
-    public static boolean migrateGridIfNeeded(Context context) {
-        SharedPreferences prefs = Utilities.getPrefs(context);
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
+        boolean migrateForPreview = idp != null;
+        if (!migrateForPreview) {
+            idp = LauncherAppState.getIDP(context);
+        }
 
-        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
-
-        if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
-                idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
-                        idp.numHotseatIcons)) {
-            // Skip if workspace and hotseat sizes have not changed.
+        if (!needsToMigrate(context, idp)) {
             return true;
         }
 
-        long migrationStartTime = System.currentTimeMillis();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+        long migrationStartTime = SystemClock.elapsedRealtime();
         try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
                 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
                 .getBinder(Settings.EXTRA_VALUE)) {
@@ -908,33 +933,40 @@
                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
 
             boolean dbChanged = false;
+            if (migrateForPreview) {
+                copyTable(transaction.getDb(), Favorites.TABLE_NAME, transaction.getDb(),
+                        Favorites.PREVIEW_TABLE_NAME, context);
+            }
 
             GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
                     srcHotseatCount, sourceSize.x, sourceSize.y);
-            if (backupTable.backupOrRestoreAsNeeded()) {
+            if (migrateForPreview ? backupTable.restoreToPreviewIfBackupExists()
+                    : backupTable.backupOrRestoreAsNeeded()) {
                 dbChanged = true;
                 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
             }
 
             HashSet<String> validPackages = getValidPackages(context);
-            // Hotseat
-            if (srcHotseatCount != idp.numHotseatIcons) {
-                // Migrate hotseat.
-                dbChanged = new GridSizeMigrationTask(context, transaction.getDb(),
-                        validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
+            // Hotseat.
+            if (srcHotseatCount != idp.numHotseatIcons
+                    && new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
+                            migrateForPreview, srcHotseatCount,
+                            idp.numHotseatIcons).migrateHotseat()) {
+                dbChanged = true;
             }
 
             // Grid size
             Point targetSize = new Point(idp.numColumns, idp.numRows);
-            if (new MultiStepMigrationTask(validPackages, context, transaction.getDb())
-                    .migrate(sourceSize, targetSize)) {
+            if (new MultiStepMigrationTask(validPackages, context, transaction.getDb(),
+                    migrateForPreview).migrate(sourceSize, targetSize)) {
                 dbChanged = true;
             }
 
             if (dbChanged) {
                 // Make sure we haven't removed everything.
                 final Cursor c = context.getContentResolver().query(
-                        Favorites.CONTENT_URI, null, null, null, null);
+                        migrateForPreview ? Favorites.PREVIEW_CONTENT_URI : Favorites.CONTENT_URI,
+                        null, null, null, null);
                 boolean hasData = c.moveToNext();
                 c.close();
                 if (!hasData) {
@@ -943,21 +975,25 @@
             }
 
             transaction.commit();
-            Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
+            if (!migrateForPreview) {
+                Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
+            }
             return true;
         } catch (Exception e) {
-            Log.e(TAG, "Error during grid migration", e);
+            Log.e(TAG, "Error during preview grid migration", e);
 
             return false;
         } finally {
-            Log.v(TAG, "Workspace migration completed in "
-                    + (System.currentTimeMillis() - migrationStartTime));
+            Log.v(TAG, "Preview workspace migration completed in "
+                    + (SystemClock.elapsedRealtime() - migrationStartTime));
 
-            // Save current configuration, so that the migration does not run again.
-            prefs.edit()
-                    .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
-                    .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
-                    .apply();
+            if (!migrateForPreview) {
+                // Save current configuration, so that the migration does not run again.
+                prefs.edit()
+                        .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+                        .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+                        .apply();
+            }
         }
     }
 
@@ -972,8 +1008,8 @@
                 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
             validPackages.add(info.packageName);
         }
-        PackageInstallerCompat.getInstance(context)
-                .updateAndGetActiveSessionCache().keySet()
+        InstallSessionHelper.INSTANCE.get(context)
+                .getActiveSessions().keySet()
                 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
         return validPackages;
     }
@@ -990,7 +1026,7 @@
                 .getBinder(Settings.EXTRA_VALUE)) {
             GridSizeMigrationTask task = new GridSizeMigrationTask(
                     context, transaction.getDb(), getValidPackages(context),
-                    Integer.MAX_VALUE, Integer.MAX_VALUE);
+                    false /* usePreviewTable */, Integer.MAX_VALUE, Integer.MAX_VALUE);
 
             // Load all the valid entries
             ArrayList<DbEntry> items = task.loadHotseatEntries();
@@ -1012,12 +1048,14 @@
         private final HashSet<String> mValidPackages;
         private final Context mContext;
         private final SQLiteDatabase mDb;
+        private final boolean mUsePreviewTable;
 
         public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
-                SQLiteDatabase db) {
+                SQLiteDatabase db, boolean usePreviewTable) {
             mValidPackages = validPackages;
             mContext = context;
             mDb = db;
+            mUsePreviewTable = usePreviewTable;
         }
 
         public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
@@ -1053,8 +1091,8 @@
         }
 
         protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
-            return new GridSizeMigrationTask(mContext, mDb,
-                    mValidPackages, sourceSize, nextSize).migrateWorkspace();
+            return new GridSizeMigrationTask(mContext, mDb, mValidPackages, mUsePreviewTable,
+                    sourceSize, nextSize).migrateWorkspace();
         }
     }
 }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
new file mode 100644
index 0000000..ebdfa8c
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -0,0 +1,765 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
+import static com.android.launcher3.Utilities.getPointString;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+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;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.WidgetManagerHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
+ * result of restoring from a larger device or device density change.
+ */
+public class GridSizeMigrationTaskV2 {
+
+    private static final String TAG = "GridSizeMigrationTaskV2";
+    private static final boolean DEBUG = true;
+
+    private final Context mContext;
+    private final SQLiteDatabase mDb;
+    private final DbReader mSrcReader;
+    private final DbReader mDestReader;
+
+    private final List<DbEntry> mHotseatItems;
+    private final List<DbEntry> mWorkspaceItems;
+
+    private final List<DbEntry> mHotseatDiff;
+    private final List<DbEntry> mWorkspaceDiff;
+
+    private final int mDestHotseatSize;
+    private final int mTrgX, mTrgY;
+
+    @VisibleForTesting
+    protected GridSizeMigrationTaskV2(Context context, SQLiteDatabase db, DbReader srcReader,
+            DbReader destReader, int destHotseatSize, Point targetSize) {
+        mContext = context;
+        mDb = db;
+        mSrcReader = srcReader;
+        mDestReader = destReader;
+
+        mHotseatItems = destReader.loadHotseatEntries();
+        mWorkspaceItems = destReader.loadAllWorkspaceEntries();
+
+        mHotseatDiff = calcDiff(mSrcReader.loadHotseatEntries(), mHotseatItems);
+        mWorkspaceDiff = calcDiff(mSrcReader.loadAllWorkspaceEntries(), mWorkspaceItems);
+        mDestHotseatSize = destHotseatSize;
+
+        mTrgX = targetSize.x;
+        mTrgY = targetSize.y;
+    }
+
+    /**
+     * Check given a new IDP, if migration is necessary.
+     */
+    public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+        return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
+    }
+
+    /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
+    public static boolean migrateGridIfNeeded(Context context) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return true;
+        }
+        return migrateGridIfNeeded(context, null);
+    }
+
+    /**
+     * When migrating the grid for preview, we copy the table
+     * {@link LauncherSettings.Favorites.TABLE_NAME} into
+     * {@link LauncherSettings.Favorites.PREVIEW_TABLE_NAME}, run grid size migration from the
+     * former to the later, then use the later table for preview.
+     *
+     * Similarly when doing the actual grid migration, the former grid option's table
+     * {@link LauncherSettings.Favorites.TABLE_NAME} is copied into the new grid option's
+     * {@link LauncherSettings.Favorites.TMP_TABLE}, we then run the grid size migration algorithm
+     * to migrate the later to the former, and load the workspace from the default
+     * {@link LauncherSettings.Favorites.TABLE_NAME}.
+     *
+     * @return false if the migration failed.
+     */
+    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
+        boolean migrateForPreview = idp != null;
+        if (!migrateForPreview) {
+            idp = LauncherAppState.getIDP(context);
+        }
+
+        if (!needsToMigrate(context, idp)) {
+            return true;
+        }
+
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+        HashSet<String> validPackages = getValidPackages(context);
+        int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
+
+        if (migrateForPreview) {
+            if (!LauncherSettings.Settings.call(
+                    context.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW, idp.dbFile).getBoolean(
+                    LauncherSettings.Settings.EXTRA_VALUE)) {
+                return false;
+            }
+        } else if (!LauncherSettings.Settings.call(
+                context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER).getBoolean(
+                LauncherSettings.Settings.EXTRA_VALUE)) {
+            return false;
+        }
+
+        long migrationStartTime = System.currentTimeMillis();
+        try (SQLiteTransaction t = (SQLiteTransaction) LauncherSettings.Settings.call(
+                context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_TRANSACTION).getBinder(
+                LauncherSettings.Settings.EXTRA_VALUE)) {
+
+            DbReader srcReader = new DbReader(t.getDb(),
+                    migrateForPreview ? LauncherSettings.Favorites.TABLE_NAME
+                            : LauncherSettings.Favorites.TMP_TABLE,
+                    context, validPackages, srcHotseatCount);
+            DbReader destReader = new DbReader(t.getDb(),
+                    migrateForPreview ? LauncherSettings.Favorites.PREVIEW_TABLE_NAME
+                            : LauncherSettings.Favorites.TABLE_NAME,
+                    context, validPackages, idp.numHotseatIcons);
+
+            Point targetSize = new Point(idp.numColumns, idp.numRows);
+            GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(),
+                    srcReader, destReader, idp.numHotseatIcons, targetSize);
+            task.migrate();
+
+            if (!migrateForPreview) {
+                dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
+            }
+
+            t.commit();
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, "Error during grid migration", e);
+
+            return false;
+        } finally {
+            Log.v(TAG, "Workspace migration completed in "
+                    + (System.currentTimeMillis() - migrationStartTime));
+
+            if (!migrateForPreview) {
+                // Save current configuration, so that the migration does not run again.
+                prefs.edit()
+                        .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+                        .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+                        .apply();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected boolean migrate() {
+        if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) {
+            return false;
+        }
+
+        // Migrate hotseat
+        HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader,
+                mDestReader, mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff);
+        hotseatSolution.find();
+
+        // Sort the items by the reading order.
+        Collections.sort(mWorkspaceDiff);
+
+        // Migrate workspace.
+        for (int screenId = 0; screenId <= mDestReader.mLastScreenId; screenId++) {
+            if (DEBUG) {
+                Log.d(TAG, "Migrating " + screenId);
+            }
+            GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+                    mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff);
+            workspaceSolution.find();
+            if (mWorkspaceDiff.isEmpty()) {
+                break;
+            }
+        }
+
+        int screenId = mDestReader.mLastScreenId + 1;
+        while (!mWorkspaceDiff.isEmpty()) {
+            GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+                    mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff);
+            workspaceSolution.find();
+            screenId++;
+        }
+        return true;
+    }
+
+    /** Return what's in the src but not in the dest */
+    private static List<DbEntry> calcDiff(List<DbEntry> src, List<DbEntry> dest) {
+        Set<String> destIntentSet = new HashSet<>();
+        Set<Map<String, Integer>> destFolderIntentSet = new HashSet<>();
+        for (DbEntry entry : dest) {
+            if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                destFolderIntentSet.add(getFolderIntents(entry));
+            } else {
+                destIntentSet.add(entry.mIntent);
+            }
+        }
+        List<DbEntry> diff = new ArrayList<>();
+        for (DbEntry entry : src) {
+            if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                if (!destFolderIntentSet.contains(getFolderIntents(entry))) {
+                    diff.add(entry);
+                }
+            } else {
+                if (!destIntentSet.contains(entry.mIntent)) {
+                    diff.add(entry);
+                }
+            }
+        }
+        return diff;
+    }
+
+    private static Map<String, Integer> getFolderIntents(DbEntry entry) {
+        Map<String, Integer> folder = new HashMap<>();
+        for (String intent : entry.mFolderItems.keySet()) {
+            folder.put(intent, entry.mFolderItems.get(intent).size());
+        }
+        return folder;
+    }
+
+    private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry,
+            String srcTableName, String destTableName) {
+        int id = copyEntryAndUpdate(db, context, entry, srcTableName, destTableName);
+
+        if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            for (Set<Integer> itemIds : entry.mFolderItems.values()) {
+                for (int itemId : itemIds) {
+                    copyEntryAndUpdate(db, context, itemId, id, srcTableName, destTableName);
+                }
+            }
+        }
+    }
+
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+            DbEntry entry, String srcTableName, String destTableName) {
+        return copyEntryAndUpdate(db, context, entry, -1, -1, srcTableName, destTableName);
+    }
+
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+            int id, int folderId, String srcTableName, String destTableName) {
+        return copyEntryAndUpdate(db, context, null, id, folderId, srcTableName, destTableName);
+    }
+
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+            DbEntry entry, int id, int folderId, String srcTableName, String destTableName) {
+        int newId = -1;
+        Cursor c = db.query(srcTableName, null,
+                LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
+                null, null, null, null);
+        while (c.moveToNext()) {
+            ContentValues values = new ContentValues();
+            DatabaseUtils.cursorRowToContentValues(c, values);
+            if (entry != null) {
+                entry.updateContentValues(values);
+            } else {
+                values.put(LauncherSettings.Favorites.CONTAINER, folderId);
+            }
+            newId = LauncherSettings.Settings.call(context.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
+                    LauncherSettings.Settings.EXTRA_VALUE);
+            values.put(LauncherSettings.Favorites._ID, newId);
+            db.insert(destTableName, null, values);
+        }
+        c.close();
+        return newId;
+    }
+
+    private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
+        db.delete(tableName,
+                Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null);
+    }
+
+    private static HashSet<String> getValidPackages(Context context) {
+        // Initialize list of valid packages. This contain all the packages which are already on
+        // the device and packages which are being installed. Any item which doesn't belong to
+        // this set is removed.
+        // Since the loader removes such items anyway, removing these items here doesn't cause
+        // any extra data loss and gives us more free space on the grid for better migration.
+        HashSet<String> validPackages = new HashSet<>();
+        for (PackageInfo info : context.getPackageManager()
+                .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
+            validPackages.add(info.packageName);
+        }
+        InstallSessionHelper.INSTANCE.get(context)
+                .getActiveSessions().keySet()
+                .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
+        return validPackages;
+    }
+
+    protected static class GridPlacementSolution {
+
+        private final SQLiteDatabase mDb;
+        private final DbReader mSrcReader;
+        private final DbReader mDestReader;
+        private final Context mContext;
+        private final GridOccupancy mOccupied;
+        private final int mScreenId;
+        private final int mTrgX;
+        private final int mTrgY;
+        private final List<DbEntry> mItemsToPlace;
+
+        private int mNextStartX;
+        private int mNextStartY;
+
+        GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader,
+                Context context, int screenId, int trgX, int trgY, List<DbEntry> itemsToPlace) {
+            mDb = db;
+            mSrcReader = srcReader;
+            mDestReader = destReader;
+            mContext = context;
+            mOccupied = new GridOccupancy(trgX, trgY);
+            mScreenId = screenId;
+            mTrgX = trgX;
+            mTrgY = trgY;
+            mNextStartX = 0;
+            mNextStartY = mTrgY - 1;
+            List<DbEntry> existedEntries = mDestReader.mWorkspaceEntriesByScreenId.get(screenId);
+            if (existedEntries != null) {
+                for (DbEntry entry : existedEntries) {
+                    mOccupied.markCells(entry, true);
+                }
+            }
+            mItemsToPlace = itemsToPlace;
+        }
+
+        public void find() {
+            Iterator<DbEntry> iterator = mItemsToPlace.iterator();
+            while (iterator.hasNext()) {
+                final DbEntry entry = iterator.next();
+                if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
+                    iterator.remove();
+                    continue;
+                }
+                if (findPlacement(entry)) {
+                    insertEntryInDb(mDb, mContext, entry, mSrcReader.mTableName,
+                            mDestReader.mTableName);
+                    iterator.remove();
+                }
+            }
+        }
+
+        /**
+         * Search for the next possible placement of an icon. (mNextStartX, mNextStartY) serves as
+         * a memoization of last placement, we can start our search for next placement from there
+         * to speed up the search.
+         */
+        private boolean findPlacement(DbEntry entry) {
+            for (int y = mNextStartY; y > 0; y--) {
+                for (int x = mNextStartX; x < mTrgX; x++) {
+                    boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
+                    boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX,
+                            entry.minSpanY);
+                    if (minFits) {
+                        entry.spanX = entry.minSpanX;
+                        entry.spanY = entry.minSpanY;
+                    }
+                    if (fits || minFits) {
+                        entry.screenId = mScreenId;
+                        entry.cellX = x;
+                        entry.cellY = y;
+                        mOccupied.markCells(entry, true);
+                        mNextStartX = x + entry.spanX;
+                        mNextStartY = y;
+                        return true;
+                    }
+                }
+                mNextStartX = 0;
+            }
+            return false;
+        }
+    }
+
+    protected static class HotseatPlacementSolution {
+
+        private final SQLiteDatabase mDb;
+        private final DbReader mSrcReader;
+        private final DbReader mDestReader;
+        private final Context mContext;
+        private final HotseatOccupancy mOccupied;
+        private final List<DbEntry> mItemsToPlace;
+
+        HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader,
+                Context context, int hotseatSize, List<DbEntry> placedHotseatItems,
+                List<DbEntry> itemsToPlace) {
+            mDb = db;
+            mSrcReader = srcReader;
+            mDestReader = destReader;
+            mContext = context;
+            mOccupied = new HotseatOccupancy(hotseatSize);
+            for (DbEntry entry : placedHotseatItems) {
+                mOccupied.markCells(entry, true);
+            }
+            mItemsToPlace = itemsToPlace;
+        }
+
+        public void find() {
+            for (int i = 0; i < mOccupied.mCells.length; i++) {
+                if (!mOccupied.mCells[i] && !mItemsToPlace.isEmpty()) {
+                    DbEntry entry = mItemsToPlace.remove(0);
+                    entry.screenId = i;
+                    // These values does not affect the item position, but we should set them
+                    // to something other than -1.
+                    entry.cellX = i;
+                    entry.cellY = 0;
+                    insertEntryInDb(mDb, mContext, entry, mSrcReader.mTableName,
+                            mDestReader.mTableName);
+                    mOccupied.markCells(entry, true);
+                }
+            }
+        }
+
+        private class HotseatOccupancy {
+
+            private final boolean[] mCells;
+
+            private HotseatOccupancy(int hotseatSize) {
+                mCells = new boolean[hotseatSize];
+            }
+
+            private void markCells(ItemInfo item, boolean value) {
+                mCells[item.screenId] = value;
+            }
+        }
+    }
+
+    protected static class DbReader {
+
+        private final SQLiteDatabase mDb;
+        private final String mTableName;
+        private final Context mContext;
+        private final HashSet<String> mValidPackages;
+        private final int mHotseatSize;
+        private int mLastScreenId = -1;
+
+        private final ArrayList<DbEntry> mHotseatEntries = new ArrayList<>();
+        private final ArrayList<DbEntry> mWorkspaceEntries = new ArrayList<>();
+        private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
+                new ArrayMap<>();
+
+        DbReader(SQLiteDatabase db, String tableName, Context context,
+                HashSet<String> validPackages, int hotseatSize) {
+            mDb = db;
+            mTableName = tableName;
+            mContext = context;
+            mValidPackages = validPackages;
+            mHotseatSize = hotseatSize;
+        }
+
+        protected ArrayList<DbEntry> loadHotseatEntries() {
+            Cursor c = queryWorkspace(
+                    new String[]{
+                            LauncherSettings.Favorites._ID,                  // 0
+                            LauncherSettings.Favorites.ITEM_TYPE,            // 1
+                            LauncherSettings.Favorites.INTENT,               // 2
+                            LauncherSettings.Favorites.SCREEN},              // 3
+                    LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+
+            final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+            final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+            final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+            final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+
+            IntArray entriesToRemove = new IntArray();
+            while (c.moveToNext()) {
+                DbEntry entry = new DbEntry();
+                entry.id = c.getInt(indexId);
+                entry.itemType = c.getInt(indexItemType);
+                entry.screenId = c.getInt(indexScreen);
+
+                if (entry.screenId >= mHotseatSize) {
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+
+                try {
+                    // calculate weight
+                    switch (entry.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+                            entry.mIntent = c.getString(indexIntent);
+                            verifyIntent(c.getString(indexIntent));
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+                            int total = getFolderItemsCount(entry);
+                            if (total == 0) {
+                                throw new Exception("Folder is empty");
+                            }
+                            break;
+                        }
+                        default:
+                            throw new Exception("Invalid item type");
+                    }
+                } catch (Exception e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Removing item " + entry.id, e);
+                    }
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+                mHotseatEntries.add(entry);
+            }
+            removeEntryFromDb(mDb, mTableName, entriesToRemove);
+            c.close();
+            return mHotseatEntries;
+        }
+
+        protected ArrayList<DbEntry> loadAllWorkspaceEntries() {
+            Cursor c = queryWorkspace(
+                    new String[]{
+                            LauncherSettings.Favorites._ID,                  // 0
+                            LauncherSettings.Favorites.ITEM_TYPE,            // 1
+                            LauncherSettings.Favorites.SCREEN,               // 2
+                            LauncherSettings.Favorites.CELLX,                // 3
+                            LauncherSettings.Favorites.CELLY,                // 4
+                            LauncherSettings.Favorites.SPANX,                // 5
+                            LauncherSettings.Favorites.SPANY,                // 6
+                            LauncherSettings.Favorites.INTENT,               // 7
+                            LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
+                            LauncherSettings.Favorites.APPWIDGET_ID},        // 9
+                        LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_DESKTOP);
+            return loadWorkspaceEntries(c);
+        }
+
+        private ArrayList<DbEntry> loadWorkspaceEntries(Cursor c) {
+            final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+            final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+            final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+            final int indexCellX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
+            final int indexCellY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+            final int indexSpanX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
+            final int indexSpanY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
+            final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+            final int indexAppWidgetProvider = c.getColumnIndexOrThrow(
+                    LauncherSettings.Favorites.APPWIDGET_PROVIDER);
+            final int indexAppWidgetId = c.getColumnIndexOrThrow(
+                    LauncherSettings.Favorites.APPWIDGET_ID);
+
+            IntArray entriesToRemove = new IntArray();
+            WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
+            while (c.moveToNext()) {
+                DbEntry entry = new DbEntry();
+                entry.id = c.getInt(indexId);
+                entry.itemType = c.getInt(indexItemType);
+                entry.screenId = c.getInt(indexScreen);
+                mLastScreenId = Math.max(mLastScreenId, entry.screenId);
+                entry.cellX = c.getInt(indexCellX);
+                entry.cellY = c.getInt(indexCellY);
+                entry.spanX = c.getInt(indexSpanX);
+                entry.spanY = c.getInt(indexSpanY);
+
+                try {
+                    // calculate weight
+                    switch (entry.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+                            entry.mIntent = c.getString(indexIntent);
+                            verifyIntent(entry.mIntent);
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
+                            entry.mProvider = c.getString(indexAppWidgetProvider);
+                            ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
+                            verifyPackage(cn.getPackageName());
+
+                            int widgetId = c.getInt(indexAppWidgetId);
+                            LauncherAppWidgetProviderInfo pInfo =
+                                    widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
+                            Point spans = null;
+                            if (pInfo != null) {
+                                spans = pInfo.getMinSpans();
+                            }
+                            if (spans != null) {
+                                entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
+                                entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
+                            } else {
+                                // Assume that the widget be resized down to 2x2
+                                entry.minSpanX = entry.minSpanY = 2;
+                            }
+
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+                            int total = getFolderItemsCount(entry);
+                            if (total == 0) {
+                                throw new Exception("Folder is empty");
+                            }
+                            break;
+                        }
+                        default:
+                            throw new Exception("Invalid item type");
+                    }
+                } catch (Exception e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Removing item " + entry.id, e);
+                    }
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+                mWorkspaceEntries.add(entry);
+                if (!mWorkspaceEntriesByScreenId.containsKey(entry.screenId)) {
+                    mWorkspaceEntriesByScreenId.put(entry.screenId, new ArrayList<>());
+                }
+                mWorkspaceEntriesByScreenId.get(entry.screenId).add(entry);
+            }
+            removeEntryFromDb(mDb, mTableName, entriesToRemove);
+            c.close();
+            return mWorkspaceEntries;
+        }
+
+        private int getFolderItemsCount(DbEntry entry) {
+            Cursor c = queryWorkspace(
+                    new String[]{LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT},
+                    LauncherSettings.Favorites.CONTAINER + " = " + entry.id);
+
+            int total = 0;
+            while (c.moveToNext()) {
+                try {
+                    int id = c.getInt(0);
+                    String intent = c.getString(1);
+                    verifyIntent(intent);
+                    total++;
+                    if (!entry.mFolderItems.containsKey(intent)) {
+                        entry.mFolderItems.put(intent, new HashSet<>());
+                    }
+                    entry.mFolderItems.get(intent).add(id);
+                } catch (Exception e) {
+                    removeEntryFromDb(mDb, mTableName, IntArray.wrap(c.getInt(0)));
+                }
+            }
+            c.close();
+            return total;
+        }
+
+        private Cursor queryWorkspace(String[] columns, String where) {
+            return mDb.query(mTableName, columns, where, null, null, null, null);
+        }
+
+        /** Verifies if the mIntent should be restored. */
+        private void verifyIntent(String intentStr)
+                throws Exception {
+            Intent intent = Intent.parseUri(intentStr, 0);
+            if (intent.getComponent() != null) {
+                verifyPackage(intent.getComponent().getPackageName());
+            } else if (intent.getPackage() != null) {
+                // Only verify package if the component was null.
+                verifyPackage(intent.getPackage());
+            }
+        }
+
+        /** Verifies if the package should be restored */
+        private void verifyPackage(String packageName)
+                throws Exception {
+            if (!mValidPackages.contains(packageName)) {
+                // TODO(b/151468819): Handle promise app icon restoration during grid migration.
+                throw new Exception("Package not available");
+            }
+        }
+    }
+
+    protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
+
+        private String mIntent;
+        private String mProvider;
+        private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
+
+        /** Comparator according to the reading order */
+        @Override
+        public int compareTo(DbEntry another) {
+            if (screenId != another.screenId) {
+                return Integer.compare(screenId, another.screenId);
+            }
+            if (cellY != another.cellY) {
+                return -Integer.compare(cellY, another.cellY);
+            }
+            return Integer.compare(cellX, another.cellX);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            DbEntry entry = (DbEntry) o;
+            return Objects.equals(mIntent, entry.mIntent);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mIntent);
+        }
+
+        public void updateContentValues(ContentValues values) {
+            values.put(LauncherSettings.Favorites.SCREEN, screenId);
+            values.put(LauncherSettings.Favorites.CELLX, cellX);
+            values.put(LauncherSettings.Favorites.CELLY, cellY);
+            values.put(LauncherSettings.Favorites.SPANX, spanX);
+            values.put(LauncherSettings.Favorites.SPANY, spanY);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 1c39d1f..165d1ea 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -16,36 +16,40 @@
 
 package com.android.launcher3.model;
 
+import static android.graphics.BitmapFactory.decodeByteArray;
+
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Intent.ShortcutIconResource;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.CursorWrapper;
-import android.graphics.BitmapFactory;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.IconCache;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
@@ -61,8 +65,9 @@
 
     private static final String TAG = "LoaderCursor";
 
-    public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
+    public final LongSparseArray<UserHandle> allUsers;
 
+    private final Uri mContentUri;
     private final Context mContext;
     private final PackageManager mPM;
     private final IconCache mIconCache;
@@ -95,8 +100,12 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor c, LauncherAppState app) {
-        super(c);
+    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
+            UserManagerState userManagerState) {
+        super(cursor);
+
+        allUsers = userManagerState.allUsers;
+        mContentUri = contentUri;
         mContext = app.getContext();
         mIconCache = app.getIconCache();
         mIDP = app.getInvariantDeviceProfile();
@@ -145,15 +154,17 @@
         }
     }
 
+    @VisibleForTesting
     public WorkspaceItemInfo loadSimpleWorkspaceItem() {
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
+        info.intent = new Intent();
         // Non-app shortcuts are only supported for current user.
         info.user = user;
         info.itemType = itemType;
         info.title = getTitle();
         // the fallback icon
         if (!loadIcon(info)) {
-            info.applyFrom(mIconCache.getDefaultIcon(info.user));
+            info.bitmap = mIconCache.getDefaultIcon(info.user);
         }
 
         // TODO: If there's an explicit component and we can't install that, delete it.
@@ -166,37 +177,30 @@
      */
     protected boolean loadIcon(WorkspaceItemInfo info) {
         try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
-            return loadIcon(info, li);
-        }
-    }
-
-    /**
-     * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
-     */
-    protected boolean loadIcon(WorkspaceItemInfo info, LauncherIcons li) {
-        if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
-            String packageName = getString(iconPackageIndex);
-            String resourceName = getString(iconResourceIndex);
-            if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
-                info.iconResource = new ShortcutIconResource();
-                info.iconResource.packageName = packageName;
-                info.iconResource.resourceName = resourceName;
-                BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
-                if (iconInfo != null) {
-                    info.applyFrom(iconInfo);
-                    return true;
+            if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
+                String packageName = getString(iconPackageIndex);
+                String resourceName = getString(iconResourceIndex);
+                if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
+                    info.iconResource = new ShortcutIconResource();
+                    info.iconResource.packageName = packageName;
+                    info.iconResource.resourceName = resourceName;
+                    BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
+                    if (iconInfo != null) {
+                        info.bitmap = iconInfo;
+                        return true;
+                    }
                 }
             }
-        }
 
-        // Failed to load from resource, try loading from DB.
-        byte[] data = getBlob(iconIndex);
-        try {
-            info.applyFrom(li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)));
-            return true;
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to decode byte array for info " + info, e);
-            return false;
+            // Failed to load from resource, try loading from DB.
+            byte[] data = getBlob(iconIndex);
+            try {
+                info.bitmap = li.createIconBitmap(decodeByteArray(data, 0, data.length));
+                return true;
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to decode byte array for info " + info, e);
+                return false;
+            }
         }
     }
 
@@ -260,7 +264,7 @@
         Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
         newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
         newIntent.setComponent(componentName);
-        LauncherActivityInfo lai = LauncherAppsCompat.getInstance(mContext)
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
                 .resolveActivity(newIntent, user);
         if ((lai == null) && !allowMissingTarget) {
             Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
@@ -273,7 +277,7 @@
         info.intent = newIntent;
 
         mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
-        if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
+        if (mIconCache.isDefaultIcon(info.bitmap, user)) {
             loadIcon(info);
         }
 
@@ -318,9 +322,8 @@
     public boolean commitDeleted() {
         if (itemsToRemove.size() > 0) {
             // Remove dead items
-            mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
-                    Utilities.createDbSelectionQuery(
-                            LauncherSettings.Favorites._ID, itemsToRemove), null);
+            mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery(
+                    LauncherSettings.Favorites._ID, itemsToRemove), null);
             return true;
         }
         return false;
@@ -345,7 +348,7 @@
             // Update restored items that no longer require special handling
             ContentValues values = new ContentValues();
             values.put(LauncherSettings.Favorites.RESTORED, 0);
-            mContext.getContentResolver().update(LauncherSettings.Favorites.CONTENT_URI, values,
+            mContext.getContentResolver().update(mContentUri, values,
                     Utilities.createDbSelectionQuery(
                             LauncherSettings.Favorites._ID, restoredRows), null);
         }
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 50e1d56..102ec31 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -16,11 +16,16 @@
 
 package com.android.launcher3.model;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
-import static com.android.launcher3.model.LoaderResults.filterCurrentWorkspaceItems;
+import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+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;
+import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
 
 import android.appwidget.AppWidgetProviderInfo;
@@ -30,45 +35,53 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
-import android.os.Process;
+import android.net.Uri;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.MutableInt;
+import android.util.TimingLogger;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.FolderInfo;
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.InstallShortcutReceiver;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
-import com.android.launcher3.icons.ComponentWithLabel;
-import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
+import com.android.launcher3.folder.FolderNameInfos;
+import com.android.launcher3.folder.FolderNameProvider;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon.ComponentWithIconCachingLogic;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherActivityCachingLogic;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.ShortcutCachingLogic;
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.logging.FileLog;
+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.PackageItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.ImportDataTask;
 import com.android.launcher3.qsb.QsbContainerView;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.LooperIdleLock;
@@ -76,6 +89,7 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -84,7 +98,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CancellationException;
-import java.util.function.Supplier;
 
 /**
  * Runnable for the thread that loads the contents of the launcher:
@@ -96,21 +109,23 @@
 public class LoaderTask implements Runnable {
     private static final String TAG = "LoaderTask";
 
-    private final LauncherAppState mApp;
+    protected final LauncherAppState mApp;
     private final AllAppsList mBgAllAppsList;
-    private final BgDataModel mBgDataModel;
+    protected final BgDataModel mBgDataModel;
 
     private FirstScreenBroadcast mFirstScreenBroadcast;
 
     private final LoaderResults mResults;
 
-    private final LauncherAppsCompat mLauncherApps;
-    private final UserManagerCompat mUserManager;
-    private final DeepShortcutManager mShortcutManager;
-    private final PackageInstallerCompat mPackageInstaller;
-    private final AppWidgetManagerCompat mAppWidgetManager;
+    private final LauncherApps mLauncherApps;
+    private final UserManager mUserManager;
+    private final UserCache mUserCache;
+
+    private final InstallSessionHelper mSessionHelper;
     private final IconCache mIconCache;
 
+    private final UserManagerState mUserManagerState = new UserManagerState();
+
     private boolean mStopped;
 
     public LoaderTask(LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel dataModel,
@@ -120,11 +135,10 @@
         mBgDataModel = dataModel;
         mResults = results;
 
-        mLauncherApps = LauncherAppsCompat.getInstance(mApp.getContext());
-        mUserManager = UserManagerCompat.getInstance(mApp.getContext());
-        mShortcutManager = DeepShortcutManager.getInstance(mApp.getContext());
-        mPackageInstaller = PackageInstallerCompat.getInstance(mApp.getContext());
-        mAppWidgetManager = AppWidgetManagerCompat.getInstance(mApp.getContext());
+        mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
+        mUserManager = mApp.getContext().getSystemService(UserManager.class);
+        mUserCache = UserCache.INSTANCE.get(mApp.getContext());
+        mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
         mIconCache = mApp.getIconCache();
     }
 
@@ -168,82 +182,107 @@
             }
         }
 
-        TraceHelper.beginSection(TAG);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
+        TimingLogger logger = new TimingLogger(TAG, "run");
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
-            TraceHelper.partitionSection(TAG, "step 1.1: loading workspace");
-            loadWorkspace();
+            List<ShortcutInfo> allShortcuts = new ArrayList<>();
+            loadWorkspace(allShortcuts);
+            loadCachedPredictions();
+            logger.addSplit("loadWorkspace");
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 1.2: bind workspace workspace");
             mResults.bindWorkspace();
+            logger.addSplit("bindWorkspace");
 
             // Notify the installer packages of packages with active installs on the first screen.
-            TraceHelper.partitionSection(TAG, "step 1.3: send first screen broadcast");
             sendFirstScreenActiveInstallsBroadcast();
+            logger.addSplit("sendFirstScreenActiveInstallsBroadcast");
 
             // Take a break
-            TraceHelper.partitionSection(TAG, "step 1 completed, wait for idle");
             waitForIdle();
+            logger.addSplit("step 1 complete");
             verifyNotStopped();
 
             // second step
-            TraceHelper.partitionSection(TAG, "step 2.1: loading all apps");
             List<LauncherActivityInfo> allActivityList = loadAllApps();
+            logger.addSplit("loadAllApps");
 
-            TraceHelper.partitionSection(TAG, "step 2.2: Binding all apps");
             verifyNotStopped();
             mResults.bindAllApps();
+            logger.addSplit("bindAllApps");
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 2.3: Update icon cache");
             IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
             setIgnorePackages(updateHandler);
             updateHandler.updateIcons(allActivityList,
                     LauncherActivityCachingLogic.newInstance(mApp.getContext()),
                     mApp.getModel()::onPackageIconsUpdated);
+            logger.addSplit("update icon cache");
+
+            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                verifyNotStopped();
+                logger.addSplit("save shortcuts in icon cache");
+                updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+                        mApp.getModel()::onPackageIconsUpdated);
+            }
 
             // Take a break
-            TraceHelper.partitionSection(TAG, "step 2 completed, wait for idle");
             waitForIdle();
+            logger.addSplit("step 2 complete");
             verifyNotStopped();
 
             // third step
-            TraceHelper.partitionSection(TAG, "step 3.1: loading deep shortcuts");
-            loadDeepShortcuts();
+            List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
+            logger.addSplit("loadDeepShortcuts");
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 3.2: bind deep shortcuts");
             mResults.bindDeepShortcuts();
+            logger.addSplit("bindDeepShortcuts");
+
+            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                verifyNotStopped();
+                logger.addSplit("save deep shortcuts in icon cache");
+                updateHandler.updateIcons(allDeepShortcuts,
+                        new ShortcutCachingLogic(), (pkgs, user) -> { });
+            }
 
             // Take a break
-            TraceHelper.partitionSection(TAG, "step 3 completed, wait for idle");
             waitForIdle();
+            logger.addSplit("step 3 complete");
             verifyNotStopped();
 
             // fourth step
-            TraceHelper.partitionSection(TAG, "step 4.1: loading widgets");
-            List<ComponentWithLabel> allWidgetsList = mBgDataModel.widgetsModel.update(mApp, null);
+            List<ComponentWithLabelAndIcon> allWidgetsList =
+                    mBgDataModel.widgetsModel.update(mApp, null);
+            logger.addSplit("load widgets");
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 4.2: Binding widgets");
             mResults.bindWidgets();
-
+            logger.addSplit("bindWidgets");
             verifyNotStopped();
 
-            TraceHelper.partitionSection(TAG, "step 4.3: save widgets in icon cache");
-            updateHandler.updateIcons(allWidgetsList, new ComponentCachingLogic(
-                    mApp.getContext(), true), mApp.getModel()::onWidgetLabelsUpdated);
+            updateHandler.updateIcons(allWidgetsList,
+                    new ComponentWithIconCachingLogic(mApp.getContext(), true),
+                    mApp.getModel()::onWidgetLabelsUpdated);
+            logger.addSplit("save widgets in icon cache");
+
+            // fifth step
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                loadFolderNames();
+            }
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 5: Finish icon cache update");
             updateHandler.finish();
+            logger.addSplit("finish icon update");
 
             transaction.commit();
         } catch (CancellationException e) {
             // Loader stopped, ignore
-            TraceHelper.partitionSection(TAG, "Cancelled");
+            logger.addSplit("Cancelled");
+        } finally {
+            logger.dumpToLog();
         }
-        TraceHelper.endSection(TAG);
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     public synchronized void stopLocked() {
@@ -251,7 +290,11 @@
         this.notify();
     }
 
-    private void loadWorkspace() {
+    private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts) {
+        loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI);
+    }
+
+    protected void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, Uri contentUri) {
         final Context context = mApp.getContext();
         final ContentResolver contentResolver = context.getContentResolver();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
@@ -267,7 +310,9 @@
             clearDb = true;
         }
 
-        if (!clearDb && !GridSizeMigrationTask.migrateGridIfNeeded(context)) {
+        if (!clearDb && (MULTI_DB_GRID_MIRATION_ALGO.get()
+                ? !GridSizeMigrationTaskV2.migrateGridIfNeeded(context)
+                : !GridSizeMigrationTask.migrateGridIfNeeded(context))) {
             // Migration failed. Clear workspace.
             clearDb = true;
         }
@@ -286,15 +331,18 @@
             mBgDataModel.clear();
 
             final HashMap<PackageUserKey, SessionInfo> installingPkgs =
-                    mPackageInstaller.updateAndGetActiveSessionCache();
+                    mSessionHelper.getActiveSessions();
+            installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
+
             final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();
-            final LoaderCursor c = new LoaderCursor(contentResolver.query(
-                    LauncherSettings.Favorites.CONTENT_URI, null, null, null, null), mApp);
+            final LoaderCursor c = new LoaderCursor(
+                    contentResolver.query(contentUri, null, null, null, null), contentUri, mApp,
+                    mUserManagerState);
 
-            HashMap<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
+            Map<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
 
             try {
                 final int appWidgetIdIndex = c.getColumnIndexOrThrow(
@@ -311,19 +359,20 @@
                         LauncherSettings.Favorites.OPTIONS);
 
                 final LongSparseArray<UserHandle> allUsers = c.allUsers;
-                final LongSparseArray<Boolean> quietMode = new LongSparseArray<>();
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
-                for (UserHandle user : mUserManager.getUserProfiles()) {
-                    long serialNo = mUserManager.getSerialNumberForUser(user);
+
+                mUserManagerState.init(mUserCache, mUserManager);
+
+                for (UserHandle user : mUserCache.getUserProfiles()) {
+                    long serialNo = mUserCache.getSerialNumberForUser(user);
                     allUsers.put(serialNo, user);
-                    quietMode.put(serialNo, mUserManager.isQuietModeEnabled(user));
 
                     boolean userUnlocked = mUserManager.isUserUnlocked(user);
 
                     // We can only query for shortcuts when the user is unlocked.
                     if (userUnlocked) {
-                        DeepShortcutManager.QueryResult pinnedShortcuts =
-                                mShortcutManager.queryForPinnedShortcuts(null, user);
+                        QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
+                                .query(ShortcutRequest.PINNED);
                         if (pinnedShortcuts.wasSuccess()) {
                             for (ShortcutInfo shortcut : pinnedShortcuts) {
                                 shortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
@@ -363,8 +412,8 @@
                                 continue;
                             }
 
-                            int disabledState = quietMode.get(c.serialNumber) ?
-                                    WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
+                            int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
+                                    ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
                             ComponentName cn = intent.getComponent();
                             targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
 
@@ -387,7 +436,7 @@
                             // If there is no target package, its an implicit intent
                             // (legacy shortcut) which is always valid
                             boolean validTarget = TextUtils.isEmpty(targetPkg) ||
-                                    mLauncherApps.isPackageEnabledForProfile(targetPkg, c.user);
+                                    mLauncherApps.isPackageEnabled(targetPkg, c.user);
 
                             // If it's a deep shortcut, we'll use pinned shortcuts to restore it
                             if (cn != null && validTarget && c.itemType
@@ -396,7 +445,7 @@
                                 // component.
 
                                 // If the component is already present
-                                if (mLauncherApps.isActivityEnabledForProfile(cn, c.user)) {
+                                if (mLauncherApps.isActivityEnabled(cn, c.user)) {
                                     // no special handling necessary for this item
                                     c.markRestored();
                                 } else {
@@ -489,21 +538,16 @@
                                         continue;
                                     }
                                     info = new WorkspaceItemInfo(pinnedShortcut, context);
-                                    final WorkspaceItemInfo finalInfo = info;
-
-                                    LauncherIcons li = LauncherIcons.obtain(context);
                                     // If the pinned deep shortcut is no longer published,
                                     // use the last saved icon instead of the default.
-                                    Supplier<ItemInfoWithIcon> fallbackIconProvider = () ->
-                                            c.loadIcon(finalInfo, li) ? finalInfo : null;
-                                    info.applyFrom(li.createShortcutIcon(pinnedShortcut,
-                                            true /* badged */, fallbackIconProvider));
-                                    li.recycle();
+                                    mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);
+
                                     if (pmHelper.isAppSuspended(
                                             pinnedShortcut.getPackage(), info.user)) {
                                         info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
                                     }
-                                    intent = info.intent;
+                                    intent = info.getIntent();
+                                    allDeepShortcuts.add(pinnedShortcut);
                                 } else {
                                     // Create a shortcut info in disabled mode for now.
                                     info = c.loadSimpleWorkspaceItem();
@@ -576,7 +620,7 @@
                             break;
 
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                            if (FeatureFlags.GO_DISABLE_WIDGETS) {
+                            if (WidgetsModel.GO_DISABLE_WIDGETS) {
                                 c.markDeleted("Only legacy shortcuts can have null package");
                                 continue;
                             }
@@ -607,7 +651,7 @@
                                     LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
 
                             if (widgetProvidersMap == null) {
-                                widgetProvidersMap = mAppWidgetManager.getAllProvidersMap();
+                                widgetProvidersMap = WidgetManagerHelper.getAllProvidersMap(context);
                             }
                             final AppWidgetProviderInfo provider = widgetProvidersMap.get(
                                     new ComponentKey(component, c.user));
@@ -757,7 +801,7 @@
                 if ((numTimesPinned == null || numTimesPinned.value == 0)
                         && !pendingShortcuts.contains(key)) {
                     // Shortcut is pinned but doesn't exist on the workspace; unpin it.
-                    mShortcutManager.unpinShortcut(key);
+                    mBgDataModel.unpinShortcut(context, key);
                 }
             }
 
@@ -797,27 +841,45 @@
 
     private void setIgnorePackages(IconCacheUpdateHandler updateHandler) {
         // Ignore packages which have a promise icon.
-        HashSet<String> packagesToIgnore = new HashSet<>();
         synchronized (mBgDataModel) {
             for (ItemInfo info : mBgDataModel.itemsIdMap) {
                 if (info instanceof WorkspaceItemInfo) {
                     WorkspaceItemInfo si = (WorkspaceItemInfo) info;
                     if (si.isPromise() && si.getTargetComponent() != null) {
-                        packagesToIgnore.add(si.getTargetComponent().getPackageName());
+                        updateHandler.addPackagesToIgnore(
+                                si.user, si.getTargetComponent().getPackageName());
                     }
                 } else if (info instanceof LauncherAppWidgetInfo) {
                     LauncherAppWidgetInfo lawi = (LauncherAppWidgetInfo) info;
                     if (lawi.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
-                        packagesToIgnore.add(lawi.providerName.getPackageName());
+                        updateHandler.addPackagesToIgnore(
+                                lawi.user, lawi.providerName.getPackageName());
                     }
                 }
             }
         }
-        updateHandler.setPackagesToIgnore(Process.myUserHandle(), packagesToIgnore);
+    }
+
+    @WorkerThread
+    private void loadCachedPredictions() {
+        synchronized (mBgDataModel) {
+            List<ComponentKey> componentKeys =
+                    mApp.getPredictionModel().getPredictionComponentKeys();
+            List<LauncherActivityInfo> l;
+            mBgDataModel.cachedPredictedItems.clear();
+            for (ComponentKey key : componentKeys) {
+                l = mLauncherApps.getActivityList(key.componentName.getPackageName(), key.user);
+                if (l.size() == 0) continue;
+                AppInfo info = new AppInfo(l.get(0), key.user,
+                        mUserManagerState.isUserQuiet(key.user));
+                mBgDataModel.cachedPredictedItems.add(info);
+                mIconCache.getTitleAndIcon(info, false);
+            }
+        }
     }
 
     private List<LauncherActivityInfo> loadAllApps() {
-        final List<UserHandle> profiles = mUserManager.getUserProfiles();
+        final List<UserHandle> profiles = mUserCache.getUserProfiles();
         List<LauncherActivityInfo> allActivityList = new ArrayList<>();
         // Clear the list of apps
         mBgAllAppsList.clear();
@@ -829,7 +891,7 @@
             if (apps == null || apps.isEmpty()) {
                 return allActivityList;
             }
-            boolean quietMode = mUserManager.isQuietModeEnabled(user);
+            boolean quietMode = mUserManagerState.isUserQuiet(user);
             // Create the ApplicationInfos
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
@@ -839,31 +901,67 @@
             allActivityList.addAll(apps);
         }
 
-        if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS) {
+        if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
             // get all active sessions and add them to the all apps list
             for (PackageInstaller.SessionInfo info :
-                    mPackageInstaller.getAllVerifiedSessions()) {
+                    mSessionHelper.getAllVerifiedSessions()) {
                 mBgAllAppsList.addPromiseApp(mApp.getContext(),
-                        PackageInstallerCompat.PackageInstallInfo.fromInstallingState(info));
+                        PackageInstallInfo.fromInstallingState(info));
             }
         }
+        for (AppInfo item : mBgDataModel.cachedPredictedItems) {
+            List<LauncherActivityInfo> l = mLauncherApps.getActivityList(
+                    item.componentName.getPackageName(), item.user);
+            for (LauncherActivityInfo info : l) {
+                boolean quietMode = mUserManagerState.isUserQuiet(item.user);
+                mBgAllAppsList.add(new AppInfo(info, item.user, quietMode), info);
+            }
+        }
+
+        mBgAllAppsList.setFlags(FLAG_QUIET_MODE_ENABLED,
+                mUserManagerState.isAnyProfileQuietModeEnabled());
+        mBgAllAppsList.setFlags(FLAG_HAS_SHORTCUT_PERMISSION,
+                hasShortcutsPermission(mApp.getContext()));
+        mBgAllAppsList.setFlags(FLAG_QUIET_MODE_CHANGE_PERMISSION,
+                mApp.getContext().checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
+                        == PackageManager.PERMISSION_GRANTED);
 
         mBgAllAppsList.getAndResetChangeFlag();
         return allActivityList;
     }
 
-    private void loadDeepShortcuts() {
+    private List<ShortcutInfo> loadDeepShortcuts() {
+        List<ShortcutInfo> allShortcuts = new ArrayList<>();
         mBgDataModel.deepShortcutMap.clear();
-        mBgDataModel.hasShortcutHostPermission = mShortcutManager.hasHostPermission();
-        if (mBgDataModel.hasShortcutHostPermission) {
-            for (UserHandle user : mUserManager.getUserProfiles()) {
+
+        if (mBgAllAppsList.hasShortcutHostPermission()) {
+            for (UserHandle user : mUserCache.getUserProfiles()) {
                 if (mUserManager.isUserUnlocked(user)) {
-                    List<ShortcutInfo> shortcuts =
-                            mShortcutManager.queryForAllShortcuts(user);
+                    List<ShortcutInfo> shortcuts = new ShortcutRequest(mApp.getContext(), user)
+                            .query(ShortcutRequest.ALL);
+                    allShortcuts.addAll(shortcuts);
                     mBgDataModel.updateDeepShortcutCounts(null, user, shortcuts);
                 }
             }
         }
+        return allShortcuts;
+    }
+
+    private void loadFolderNames() {
+        FolderNameProvider provider = FolderNameProvider.newInstance(mApp.getContext(),
+                mBgAllAppsList.data, mBgDataModel.folders);
+
+        synchronized (mBgDataModel) {
+            for (int i = 0; i < mBgDataModel.folders.size(); i++) {
+                FolderNameInfos suggestionInfos = new FolderNameInfos();
+                FolderInfo info = mBgDataModel.folders.valueAt(i);
+                if (info.suggestedFolderNames == null) {
+                    provider.getSuggestedFolderName(mApp.getContext(), info.contents,
+                            suggestionInfos);
+                    info.suggestedFolderNames = suggestionInfos;
+                }
+            }
+        }
     }
 
     public static boolean isValidProvider(AppWidgetProviderInfo provider) {
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index 2bd6cd4..713492b 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,14 +18,15 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 
 import java.util.concurrent.Executor;
 
-import androidx.annotation.WorkerThread;
-
 /**
  * Utility class to preload LauncherModel
  */
@@ -50,7 +51,7 @@
     @Override
     public final void run() {
         mModel.startLoaderForResultsIfNotLoaded(
-                new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+                new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
         Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
         onComplete(mModel.isModelLoaded());
     }
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
new file mode 100644
index 0000000..4efeba5
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * Utils class for {@link com.android.launcher3.LauncherModel}.
+ */
+public class ModelUtils {
+
+    /**
+     * Filters the set of items who are directly or indirectly (via another container) on the
+     * specified screen.
+     */
+    public static <T extends ItemInfo> void filterCurrentWorkspaceItems(int currentScreenId,
+            ArrayList<T> allWorkspaceItems,
+            ArrayList<T> currentScreenItems,
+            ArrayList<T> otherScreenItems) {
+        // Purge any null ItemInfos
+        Iterator<T> iter = allWorkspaceItems.iterator();
+        while (iter.hasNext()) {
+            ItemInfo i = iter.next();
+            if (i == null) {
+                iter.remove();
+            }
+        }
+        // Order the set of items by their containers first, this allows use to walk through the
+        // list sequentially, build up a list of containers that are in the specified screen,
+        // as well as all items in those containers.
+        IntSet itemsOnScreen = new IntSet();
+        Collections.sort(allWorkspaceItems,
+                (lhs, rhs) -> Integer.compare(lhs.container, rhs.container));
+        for (T info : allWorkspaceItems) {
+            if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                if (info.screenId == currentScreenId) {
+                    currentScreenItems.add(info);
+                    itemsOnScreen.add(info.id);
+                } else {
+                    otherScreenItems.add(info);
+                }
+            } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                currentScreenItems.add(info);
+                itemsOnScreen.add(info.id);
+            } else {
+                if (itemsOnScreen.contains(info.container)) {
+                    currentScreenItems.add(info);
+                    itemsOnScreen.add(info.id);
+                } else {
+                    otherScreenItems.add(info);
+                }
+            }
+        }
+    }
+
+    /**
+     * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right)
+     */
+    public static void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile,
+            ArrayList<ItemInfo> workspaceItems) {
+        final int screenCols = profile.numColumns;
+        final int screenCellCount = profile.numColumns * profile.numRows;
+        Collections.sort(workspaceItems, (lhs, rhs) -> {
+            if (lhs.container == rhs.container) {
+                // Within containers, order by their spatial position in that container
+                switch (lhs.container) {
+                    case LauncherSettings.Favorites.CONTAINER_DESKTOP: {
+                        int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols
+                                + lhs.cellX);
+                        int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols
+                                + rhs.cellX);
+                        return Integer.compare(lr, rr);
+                    }
+                    case LauncherSettings.Favorites.CONTAINER_HOTSEAT: {
+                        // We currently use the screen id as the rank
+                        return Integer.compare(lhs.screenId, rhs.screenId);
+                    }
+                    default:
+                        if (FeatureFlags.IS_STUDIO_BUILD) {
+                            throw new RuntimeException(
+                                    "Unexpected container type when sorting workspace items.");
+                        }
+                        return 0;
+                }
+            } else {
+                // Between containers, order by hotseat, desktop
+                return Integer.compare(lhs.container, rhs.container);
+            }
+        });
+    }
+
+    /**
+     * Iterates though current workspace items and returns available hotseat ranks for prediction.
+     */
+    public static IntArray getMissingHotseatRanks(List<ItemInfo> items, int len) {
+        IntSet seen = new IntSet();
+        items.stream().filter(
+                info -> info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
+                .forEach(i -> seen.add(i.screenId));
+        IntArray result = new IntArray(len);
+        IntStream.range(0, len).filter(i -> !seen.contains(i)).forEach(result::add);
+        return result;
+    }
+}
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index bdf3a69..2c99df7 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -27,21 +27,20 @@
 import android.os.Looper;
 import android.util.Log;
 
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.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.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -116,8 +115,9 @@
         ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId);
         if (modelItem != null && item != modelItem) {
             // check all the data is consistent
-            if (!Utilities.IS_DEBUG_DEVICE && !FeatureFlags.IS_DOGFOOD_BUILD &&
-                    modelItem instanceof WorkspaceItemInfo && item instanceof WorkspaceItemInfo) {
+            if (!Utilities.IS_DEBUG_DEVICE && !FeatureFlags.IS_STUDIO_BUILD
+                    && modelItem instanceof WorkspaceItemInfo
+                    && item instanceof WorkspaceItemInfo) {
                 if (modelItem.title.toString().equals(item.title.toString()) &&
                         modelItem.getIntent().filterEquals(item.getIntent()) &&
                         modelItem.id == item.id &&
@@ -321,7 +321,7 @@
      */
     public void prepareToUndoDelete() {
         if (!mPreparingToUndo) {
-            if (!mDeleteRunnables.isEmpty() && FeatureFlags.IS_DOGFOOD_BUILD) {
+            if (!mDeleteRunnables.isEmpty() && FeatureFlags.IS_STUDIO_BUILD) {
                 throw new IllegalStateException("There are still uncommitted delete operations!");
             }
             mDeleteRunnables.clear();
@@ -350,12 +350,15 @@
         mDeleteRunnables.clear();
     }
 
-    public void abortDelete(int pageToBindFirst) {
+    /**
+     * Aborts a previous delete operation pending commit
+     */
+    public void abortDelete() {
         mPreparingToUndo = false;
         mDeleteRunnables.clear();
         // We do a full reload here instead of just a rebind because Folders change their internal
         // state when dragging an item out, which clobbers the rebind unless we load from the DB.
-        mModel.forceReload(pageToBindFirst);
+        mModel.forceReload();
     }
 
     private class UpdateItemRunnable extends UpdateItemBaseRunnable {
@@ -472,7 +475,7 @@
         }
 
         void verifyModel() {
-            if (!mVerifyChanges || mModel.getCallback() == null) {
+            if (!mVerifyChanges || !mModel.hasCallbacks()) {
                 return;
             }
 
@@ -488,11 +491,9 @@
                     // Bound model has not changed during the job
                     return;
                 }
+
                 // Bound model was changed between submitting the job and executing the job
-                Callbacks callbacks = mModel.getCallback();
-                if (callbacks != null) {
-                    callbacks.rebindModel();
-                }
+                mModel.rebindCallbacks();
             });
         }
     }
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 802cbc7..203f1ca 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -19,16 +19,14 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.PromiseAppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.util.InstantAppResolver;
 
 import java.util.HashSet;
@@ -46,7 +44,7 @@
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-        if (mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLED) {
+        if (mInstallInfo.state == PackageInstallInfo.STATUS_INSTALLED) {
             try {
                 // For instant apps we do not get package-add. Use setting events to update
                 // any pinned icons.
@@ -79,7 +77,7 @@
                     if (si.hasPromiseIconUi() && (cn != null)
                             && mInstallInfo.packageName.equals(cn.getPackageName())) {
                         si.setInstallProgress(mInstallInfo.progress);
-                        if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
+                        if (mInstallInfo.state == PackageInstallInfo.STATUS_FAILED) {
                             // Mark this info as broken.
                             si.status &= ~WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE;
                         }
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index d6ebaaf..7cd467e 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,33 +15,35 @@
  */
 package com.android.launcher3.model;
 
-import static com.android.launcher3.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
+import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
+import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 
 import com.android.launcher3.InstallShortcutReceiver;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.testing.TestProtocol;
+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.pm.UserCache;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -55,9 +57,6 @@
 import java.util.HashSet;
 import java.util.List;
 
-import static com.android.launcher3.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
-import static com.android.launcher3.WorkspaceItemInfo.FLAG_RESTORED_ICON;
-
 /**
  * Handles updates due to changes in package manager (app installed/updated/removed)
  * or when a user availability changes.
@@ -88,10 +87,6 @@
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.APP_NOT_DISABLED, "PackageUpdatedTask: " + mOp + ", " +
-                    Arrays.toString(mPackages));
-        }
         final Context context = app.getContext();
         final IconCache iconCache = app.getIconCache();
 
@@ -107,7 +102,7 @@
                 for (int i = 0; i < N; i++) {
                     if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
                     iconCache.updateIconsForPkg(packages[i], mUser);
-                    if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS) {
+                    if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                         appsList.removePackage(packages[i], mUser);
                     }
                     appsList.addPackage(context, packages[i], mUser);
@@ -156,14 +151,21 @@
                 if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
                 appsList.updateDisabledFlags(matcher, flagOp);
                 break;
-            case OP_USER_AVAILABILITY_CHANGE:
-                flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
+            case OP_USER_AVAILABILITY_CHANGE: {
+                UserManagerState ums = new UserManagerState();
+                ums.init(UserCache.INSTANCE.get(context),
+                        context.getSystemService(UserManager.class));
+                flagOp = ums.isUserQuiet(mUser)
                         ? FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER)
                         : FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER);
                 // We want to update all packages for this user.
                 matcher = ItemInfoMatcher.ofUser(mUser);
                 appsList.updateDisabledFlags(matcher, flagOp);
+
+                // We are not synchronizing here, as int operations are atomic
+                appsList.setFlags(FLAG_QUIET_MODE_ENABLED, ums.isAnyProfileQuietModeEnabled());
                 break;
+            }
         }
 
         bindApplicationsIfNeeded();
@@ -191,7 +193,7 @@
                             BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
                             li.recycle();
                             if (iconInfo != null) {
-                                si.applyFrom(iconInfo);
+                                si.bitmap = iconInfo;
                                 infoUpdated = true;
                             }
                         }
@@ -210,10 +212,11 @@
                             if (si.isPromise() && isNewApkAvailable) {
                                 boolean isTargetValid = true;
                                 if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                                    List<ShortcutInfo> shortcut = DeepShortcutManager
-                                            .getInstance(context).queryForPinnedShortcuts(
-                                                    cn.getPackageName(),
-                                                    Arrays.asList(si.getDeepShortcutId()), mUser);
+                                    List<ShortcutInfo> shortcut =
+                                            new ShortcutRequest(context, mUser)
+                                                    .forPackage(cn.getPackageName(),
+                                                            si.getDeepShortcutId())
+                                                    .query(ShortcutRequest.PINNED);
                                     if (shortcut.isEmpty()) {
                                         isTargetValid = false;
                                     } else {
@@ -221,8 +224,8 @@
                                         infoUpdated = true;
                                     }
                                 } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) {
-                                    isTargetValid = LauncherAppsCompat.getInstance(context)
-                                            .isActivityEnabledForProfile(cn, mUser);
+                                    isTargetValid = context.getSystemService(LauncherApps.class)
+                                            .isActivityEnabled(cn, mUser);
                                 }
                                 if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
                                     if (updateWorkspaceItemIntent(context, si, packageName)) {
@@ -234,7 +237,7 @@
                                 } else if (!isTargetValid) {
                                     removedShortcuts.put(si.id, true);
                                     FileLog.e(TAG, "Restored shortcut no longer valid "
-                                            + si.intent);
+                                            + si.getIntent());
                                     continue;
                                 } else {
                                     si.status = WorkspaceItemInfo.DEFAULT;
@@ -305,9 +308,9 @@
             // removedPackages is a super-set of removedComponents
         } else if (mOp == OP_UPDATE) {
             // Mark disabled packages in the broadcast to be removed
-            final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
+            final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
             for (int i=0; i<N; i++) {
-                if (!launcherApps.isPackageEnabledForProfile(packages[i], mUser)) {
+                if (!launcherApps.isPackageEnabled(packages[i], mUser)) {
                     removedPackages.add(packages[i]);
                 }
             }
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
new file mode 100644
index 0000000..1429843
--- /dev/null
+++ b/src/com/android/launcher3/model/PredictionModel.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.UserHandle;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Model Helper for app predictions
+ */
+public class PredictionModel implements ResourceBasedOverride {
+
+    private static final String CACHED_ITEMS_KEY = "predicted_item_keys";
+    private static final int MAX_CACHE_ITEMS = 5;
+
+    protected Context mContext;
+    private SharedPreferences mDevicePrefs;
+    private UserCache mUserCache;
+
+
+    /**
+     * Retrieve instance of this object that can be overridden in runtime based on the build
+     * variant of the application.
+     */
+    public static PredictionModel newInstance(Context context) {
+        PredictionModel model = Overrides.getObject(PredictionModel.class, context,
+                R.string.prediction_model_class);
+        model.init(context);
+        return model;
+    }
+
+    protected void init(Context context) {
+        mContext = context;
+        mDevicePrefs = Utilities.getDevicePrefs(mContext);
+        mUserCache = UserCache.INSTANCE.get(mContext);
+
+    }
+    /**
+     * Formats and stores a list of component key in device preferences.
+     */
+    @AnyThread
+    public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
+        MODEL_EXECUTOR.execute(() -> {
+            StringBuilder builder = new StringBuilder();
+            int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
+            for (int i = 0; i < count; i++) {
+                builder.append(serializeComponentKeyToString(componentKeys.get(i)));
+                builder.append("\n");
+            }
+            mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
+        });
+    }
+
+    /**
+     * parses and returns ComponentKeys saved by
+     * {@link PredictionModel#cachePredictionComponentKeys(List)}
+     */
+    @WorkerThread
+    public List<ComponentKey> getPredictionComponentKeys() {
+        Preconditions.assertWorkerThread();
+        ArrayList<ComponentKey> items = new ArrayList<>();
+        String cachedBlob = mDevicePrefs.getString(CACHED_ITEMS_KEY, "");
+        for (String line : cachedBlob.split("\n")) {
+            ComponentKey key = getComponentKeyFromSerializedString(line);
+            if (key != null) {
+                items.add(key);
+            }
+
+        }
+        return items;
+    }
+
+    private String serializeComponentKeyToString(ComponentKey componentKey) {
+        long userSerialNumber = mUserCache.getSerialNumberForUser(componentKey.user);
+        return componentKey.componentName.flattenToString() + "#" + userSerialNumber;
+    }
+
+    private ComponentKey getComponentKeyFromSerializedString(String str) {
+        int sep = str.indexOf('#');
+        if (sep < 0 || (sep + 1) >= str.length()) {
+            return null;
+        }
+        ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep));
+        if (componentName == null) {
+            return null;
+        }
+        try {
+            long serialNumber = Long.parseLong(str.substring(sep + 1));
+            UserHandle userHandle = mUserCache.getUserForSerialNumber(serialNumber);
+            return userHandle != null ? new ComponentKey(componentName, userHandle) : null;
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index 3aedae6..eb3cb52 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -19,11 +19,11 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
 
@@ -53,7 +53,7 @@
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
+        final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
         for (Entry<UserHandle, ArrayList<String>> entry : mPackages.entrySet()) {
             UserHandle user = entry.getKey();
@@ -62,7 +62,7 @@
             final ArrayList<String> packagesUnavailable = new ArrayList<>();
 
             for (String pkg : new HashSet<>(entry.getValue())) {
-                if (!launcherApps.isPackageEnabledForProfile(pkg, user)) {
+                if (!launcherApps.isPackageEnabled(pkg, user)) {
                     if (pmHelper.isAppOnSdcard(pkg, user)) {
                         packagesUnavailable.add(pkg);
                     } else {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 6c358b1..1cbe5c2 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -19,13 +19,12 @@
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiHashMap;
 
@@ -54,8 +53,6 @@
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         final Context context = app.getContext();
-        DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
-
         // Find WorkspaceItemInfo's that have changed on the workspace.
         HashSet<ShortcutKey> removedKeys = new HashSet<>();
         MultiHashMap<ShortcutKey, WorkspaceItemInfo> keyToShortcutInfo = new MultiHashMap<>();
@@ -74,8 +71,9 @@
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
         if (!keyToShortcutInfo.isEmpty()) {
             // Update the workspace to reflect the changes to updated shortcuts residing on it.
-            List<ShortcutInfo> shortcuts = deepShortcutManager.queryForFullDetails(
-                    mPackageName, new ArrayList<>(allIds), mUser);
+            List<ShortcutInfo> shortcuts = new ShortcutRequest(context, mUser)
+                    .forPackage(mPackageName, new ArrayList<>(allIds))
+                    .query(ShortcutRequest.ALL);
             for (ShortcutInfo fullDetails : shortcuts) {
                 ShortcutKey key = ShortcutKey.fromInfo(fullDetails);
                 List<WorkspaceItemInfo> workspaceItemInfos = keyToShortcutInfo.remove(key);
@@ -90,12 +88,7 @@
                 }
                 for (final WorkspaceItemInfo workspaceItemInfo : workspaceItemInfos) {
                     workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context);
-                    // If the shortcut is pinned but no longer has an icon in the system,
-                    // keep the current icon instead of reverting to the default icon.
-                    LauncherIcons li = LauncherIcons.obtain(context);
-                    workspaceItemInfo.applyFrom(li.createShortcutIcon(fullDetails, true,
-                            () -> workspaceItemInfo));
-                    li.recycle();
+                    app.getIconCache().getShortcutIcon(workspaceItemInfo, fullDetails);
                     updatedWorkspaceItemInfos.add(workspaceItemInfo);
                 }
             }
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 4b773d7..7ec884f 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -15,20 +15,19 @@
  */
 package com.android.launcher3.model;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
 
 import android.content.Context;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -43,21 +42,21 @@
 public class UserLockStateChangedTask extends BaseModelUpdateTask {
 
     private final UserHandle mUser;
+    private boolean mIsUserUnlocked;
 
-    public UserLockStateChangedTask(UserHandle user) {
+    public UserLockStateChangedTask(UserHandle user, boolean isUserUnlocked) {
         mUser = user;
+        mIsUserUnlocked = isUserUnlocked;
     }
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         Context context = app.getContext();
-        boolean isUserUnlocked = UserManagerCompat.getInstance(context).isUserUnlocked(mUser);
-        DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
 
         HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
-        if (isUserUnlocked) {
-            DeepShortcutManager.QueryResult shortcuts =
-                    deepShortcutManager.queryForPinnedShortcuts(null, mUser);
+        if (mIsUserUnlocked) {
+            QueryResult shortcuts = new ShortcutRequest(context, mUser)
+                    .query(ShortcutRequest.PINNED);
             if (shortcuts.wasSuccess()) {
                 for (ShortcutInfo shortcut : shortcuts) {
                     pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
@@ -66,7 +65,7 @@
                 // Shortcut manager can fail due to some race condition when the lock state
                 // changes too frequently. For the purpose of the update,
                 // consider it as still locked.
-                isUserUnlocked = false;
+                mIsUserUnlocked = false;
             }
         }
 
@@ -78,7 +77,7 @@
             if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
                     && mUser.equals(itemInfo.user)) {
                 WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
-                if (isUserUnlocked) {
+                if (mIsUserUnlocked) {
                     ShortcutKey key = ShortcutKey.fromItemInfo(si);
                     ShortcutInfo shortcut = pinnedShortcuts.get(key);
                     // We couldn't verify the shortcut during loader. If its no longer available
@@ -89,11 +88,7 @@
                     }
                     si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
                     si.updateFromDeepShortcutInfo(shortcut, context);
-                    // If the shortcut is pinned but no longer has an icon in the system,
-                    // keep the current icon instead of reverting to the default icon.
-                    LauncherIcons li = LauncherIcons.obtain(context);
-                    si.applyFrom(li.createShortcutIcon(shortcut, true, () -> si));
-                    li.recycle();
+                    app.getIconCache().getShortcutIcon(si, shortcut);
                 } else {
                     si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
                 }
@@ -113,9 +108,10 @@
             }
         }
 
-        if (isUserUnlocked) {
+        if (mIsUserUnlocked) {
             dataModel.updateDeepShortcutCounts(
-                    null, mUser, deepShortcutManager.queryForAllShortcuts(mUser));
+                    null, mUser,
+                    new ShortcutRequest(context, mUser).query(ShortcutRequest.ALL));
         }
         bindDeepShortcuts(dataModel);
     }
diff --git a/src/com/android/launcher3/model/UserManagerState.java b/src/com/android/launcher3/model/UserManagerState.java
new file mode 100644
index 0000000..3a4206c
--- /dev/null
+++ b/src/com/android/launcher3/model/UserManagerState.java
@@ -0,0 +1,73 @@
+/*
+ * 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.model;
+
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.LongSparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.launcher3.pm.UserCache;
+
+/**
+ * Utility class to manager store and user manager state at any particular time
+ */
+public class UserManagerState {
+
+    public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
+
+    private final LongSparseArray<Boolean> mQuietUsersSerialNoMap = new LongSparseArray<>();
+    private final SparseBooleanArray mQuietUsersHashCodeMap = new SparseBooleanArray();
+
+    /**
+     * Initialises the state values for all users
+     */
+    public void init(UserCache userCache, UserManager userManager) {
+        for (UserHandle user : userCache.getUserProfiles()) {
+            long serialNo = userCache.getSerialNumberForUser(user);
+            boolean isUserQuiet = userManager.isQuietModeEnabled(user);
+            allUsers.put(serialNo, user);
+            mQuietUsersHashCodeMap.put(user.hashCode(), isUserQuiet);
+            mQuietUsersSerialNoMap.put(serialNo, isUserQuiet);
+        }
+    }
+
+    /**
+     * Returns true if quiet mode is enabled for the provided user
+     */
+    public boolean isUserQuiet(long serialNo) {
+        return mQuietUsersSerialNoMap.get(serialNo);
+    }
+
+    /**
+     * Returns true if quiet mode is enabled for the provided user
+     */
+    public boolean isUserQuiet(UserHandle user) {
+        return mQuietUsersHashCodeMap.get(user.hashCode());
+    }
+
+    /**
+     * Returns true if any user profile has quiet mode enabled.
+     */
+    public boolean isAnyProfileQuietModeEnabled() {
+        for (int i = mQuietUsersHashCodeMap.size() - 1; i >= 0; i--) {
+            if (mQuietUsersHashCodeMap.valueAt(i)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index e38529b..37c089e 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -2,27 +2,20 @@
 
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
-import android.os.Process;
-import android.os.UserHandle;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ComponentKey;
 
-import java.text.Collator;
-
 /**
  * An wrapper over various items displayed in a widget picker,
  * {@link LauncherAppWidgetProviderInfo} & {@link ActivityInfo}. This provides easier access to
  * common attributes like spanX and spanY.
  */
-public class WidgetItem extends ComponentKey implements Comparable<WidgetItem> {
-
-    private static UserHandle sMyUserHandle;
-    private static Collator sCollator;
+public class WidgetItem extends ComponentKey {
 
     public final LauncherAppWidgetProviderInfo widgetInfo;
     public final ShortcutConfigActivityInfo activityInfo;
@@ -50,34 +43,4 @@
         activityInfo = info;
         spanX = spanY = 1;
     }
-
-    @Override
-    public int compareTo(WidgetItem another) {
-        if (sMyUserHandle == null) {
-            // Delay these object creation until required.
-            sMyUserHandle = Process.myUserHandle();
-            sCollator = Collator.getInstance();
-        }
-
-        // Independent of how the labels compare, if only one of the two widget info belongs to
-        // work profile, put that one in the back.
-        boolean thisWorkProfile = !sMyUserHandle.equals(user);
-        boolean otherWorkProfile = !sMyUserHandle.equals(another.user);
-        if (thisWorkProfile ^ otherWorkProfile) {
-            return thisWorkProfile ? 1 : -1;
-        }
-
-        int labelCompare = sCollator.compare(label, another.label);
-        if (labelCompare != 0) {
-            return labelCompare;
-        }
-
-        // If the label is same, put the smaller widget before the larger widget. If the area is
-        // also same, put the widget with smaller height before.
-        int thisArea = spanX * spanY;
-        int otherArea = another.spanX * another.spanY;
-        return thisArea == otherArea
-                ? Integer.compare(spanY, another.spanY)
-                : Integer.compare(thisArea, otherArea);
-    }
 }
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
similarity index 80%
rename from src/com/android/launcher3/AppInfo.java
rename to src/com/android/launcher3/model/data/AppInfo.java
index c8e7619..b17b062 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -24,8 +26,12 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 
-import com.android.launcher3.compat.UserManagerCompat;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
 
@@ -36,8 +42,8 @@
  */
 public class AppInfo extends ItemInfoWithIcon {
 
-    public static AppInfo[] EMPTY_ARRAY = new AppInfo[0];
-    public static Comparator<AppInfo> COMPONENT_KEY_COMPARATOR = (a, b) -> {
+    public static final AppInfo[] EMPTY_ARRAY = new AppInfo[0];
+    public static final Comparator<AppInfo> COMPONENT_KEY_COMPARATOR = (a, b) -> {
         int uc = a.user.hashCode() - b.user.hashCode();
         return uc != 0 ? uc : a.componentName.compareTo(b.componentName);
     };
@@ -65,12 +71,12 @@
      * Must not hold the Context.
      */
     public AppInfo(Context context, LauncherActivityInfo info, UserHandle user) {
-        this(info, user, UserManagerCompat.getInstance(context).isQuietModeEnabled(user));
+        this(info, user, context.getSystemService(UserManager.class).isQuietModeEnabled(user));
     }
 
     public AppInfo(LauncherActivityInfo info, UserHandle user, boolean quietModeEnabled) {
         this.componentName = info.getComponentName();
-        this.container = ItemInfo.NO_ID;
+        this.container = CONTAINER_ALL_APPS;
         this.user = user;
         intent = makeLaunchIntent(info);
 
@@ -89,6 +95,15 @@
         runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
+    @VisibleForTesting
+    public AppInfo(ComponentName componentName, CharSequence title,
+            UserHandle user, Intent intent) {
+        this.componentName = componentName;
+        this.title = title;
+        this.user = user;
+        this.intent = intent;
+    }
+
     @Override
     protected String dumpProperties() {
         return super.dumpProperties() + " componentName=" + componentName;
@@ -110,7 +125,8 @@
         return new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_LAUNCHER)
                 .setComponent(cn)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
     }
 
     public static void updateRuntimeFlagsForActivityTarget(
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
new file mode 100644
index 0000000..41ccbd7
--- /dev/null
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model.data;
+
+import static android.text.TextUtils.isEmpty;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
+import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
+import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
+
+import android.os.Process;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+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.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.userevent.LauncherLogProto;
+import com.android.launcher3.userevent.LauncherLogProto.Target;
+import com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState;
+import com.android.launcher3.userevent.LauncherLogProto.Target.ToFolderLabelState;
+import com.android.launcher3.util.ContentWriter;
+
+import java.util.ArrayList;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
+
+
+/**
+ * Represents a folder containing shortcuts or apps.
+ */
+public class FolderInfo extends ItemInfo {
+
+    public static final int NO_FLAGS = 0x00000000;
+
+    /**
+     * The folder is locked in sorted mode
+     */
+    public static final int FLAG_ITEMS_SORTED = 0x00000001;
+
+    /**
+     * It is a work folder
+     */
+    public static final int FLAG_WORK_FOLDER = 0x00000002;
+
+    /**
+     * The multi-page animation has run for this folder
+     */
+    public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004;
+
+    public static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
+
+    /**
+     * Different states of folder label.
+     */
+    public enum LabelState {
+        // Folder's label is not yet assigned( i.e., title == null). Eligible for auto-labeling.
+        UNLABELED(Attribute.UNLABELED),
+
+        // Folder's label is empty(i.e., title == ""). Not eligible for auto-labeling.
+        EMPTY(EMPTY_LABEL),
+
+        // Folder's label is one of the non-empty suggested values.
+        SUGGESTED(SUGGESTED_LABEL),
+
+        // Folder's label is non-empty, manually entered by the user
+        // and different from any of suggested values.
+        MANUAL(MANUAL_LABEL);
+
+        private final LauncherAtom.Attribute mLogAttribute;
+
+        LabelState(Attribute logAttribute) {
+            this.mLogAttribute = logAttribute;
+        }
+    }
+
+    public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest";
+
+    public int options;
+
+    public FolderNameInfos suggestedFolderNames;
+
+    /**
+     * The apps and shortcuts
+     */
+    public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
+
+    private ArrayList<FolderListener> mListeners = new ArrayList<>();
+
+    public FolderInfo() {
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
+        user = Process.myUserHandle();
+    }
+
+    /**
+     * Add an app or shortcut
+     *
+     * @param item
+     */
+    public void add(WorkspaceItemInfo item, boolean animate) {
+        add(item, contents.size(), animate);
+    }
+
+    /**
+     * Add an app or shortcut for a specified rank.
+     */
+    public void add(WorkspaceItemInfo item, int rank, boolean animate) {
+        rank = Utilities.boundToRange(rank, 0, contents.size());
+        contents.add(rank, item);
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onAdd(item, rank);
+        }
+        itemsChanged(animate);
+    }
+
+    /**
+     * Remove an app or shortcut. Does not change the DB.
+     *
+     * @param item
+     */
+    public void remove(WorkspaceItemInfo item, boolean animate) {
+        contents.remove(item);
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onRemove(item);
+        }
+        itemsChanged(animate);
+    }
+
+    @Override
+    public void onAddToDatabase(ContentWriter writer) {
+        super.onAddToDatabase(writer);
+        writer.put(LauncherSettings.Favorites.TITLE, title)
+                .put(LauncherSettings.Favorites.OPTIONS, options);
+    }
+
+    public void addListener(FolderListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(FolderListener listener) {
+        mListeners.remove(listener);
+    }
+
+    public void itemsChanged(boolean animate) {
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onItemsChanged(animate);
+        }
+    }
+
+    public interface FolderListener {
+        public void onAdd(WorkspaceItemInfo item, int rank);
+        public void onRemove(WorkspaceItemInfo item);
+        public void onItemsChanged(boolean animate);
+    }
+
+    public boolean hasOption(int optionFlag) {
+        return (options & optionFlag) != 0;
+    }
+
+    /**
+     * @param option flag to set or clear
+     * @param isEnabled whether to set or clear the flag
+     * @param writer if not null, save changes to the db.
+     */
+    public void setOption(int option, boolean isEnabled, ModelWriter writer) {
+        int oldOptions = options;
+        if (isEnabled) {
+            options |= option;
+        } else {
+            options &= ~option;
+        }
+        if (writer != null && oldOptions != options) {
+            writer.updateItemInDatabase(this);
+        }
+    }
+
+    @Override
+    protected String dumpProperties() {
+        return String.format("%s; labelState=%s", super.dumpProperties(), getLabelState());
+    }
+
+    @Override
+    public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
+        return getDefaultItemInfoBuilder()
+                .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
+                .setRank(rank)
+                .setAttribute(getLabelState().mLogAttribute)
+                .setContainerInfo(getContainerInfo())
+                .build();
+    }
+
+    @Override
+    public void setTitle(@Nullable CharSequence title, ModelWriter modelWriter) {
+        // Updating label from null to empty is considered as false touch.
+        // Retaining null title(ie., UNLABELED state) allows auto-labeling when new items added.
+        if (isEmpty(title) && this.title == null) {
+            return;
+        }
+
+        // Updating title to same value does not change any states.
+        if (title != null && title.equals(this.title)) {
+            return;
+        }
+
+        this.title = title;
+        LabelState newLabelState =
+                title == null ? LabelState.UNLABELED
+                        : title.length() == 0 ? LabelState.EMPTY :
+                                getAcceptedSuggestionIndex().isPresent() ? LabelState.SUGGESTED
+                                        : LabelState.MANUAL;
+
+        if (newLabelState.equals(LabelState.MANUAL)) {
+            options |= FLAG_MANUAL_FOLDER_NAME;
+        } else {
+            options &= ~FLAG_MANUAL_FOLDER_NAME;
+        }
+        if (modelWriter != null) {
+            modelWriter.updateItemInDatabase(this);
+        }
+    }
+
+    /**
+     * Returns current state of the current folder label.
+     */
+    public LabelState getLabelState() {
+        return title == null ? LabelState.UNLABELED
+                : title.length() == 0 ? LabelState.EMPTY :
+                        hasOption(FLAG_MANUAL_FOLDER_NAME) ? LabelState.MANUAL
+                                : LabelState.SUGGESTED;
+    }
+
+    @Override
+    public ItemInfo makeShallowCopy() {
+        FolderInfo folderInfo = new FolderInfo();
+        folderInfo.copyFrom(this);
+        folderInfo.contents = this.contents;
+        return folderInfo;
+    }
+
+    /**
+     * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
+     */
+    @Override
+    public LauncherAtom.ItemInfo buildProto() {
+        return buildProto(null);
+    }
+
+    /**
+     * Returns index of the accepted suggestion.
+     */
+    public OptionalInt getAcceptedSuggestionIndex() {
+        String newLabel = checkNotNull(title,
+                "Expected valid folder label, but found null").toString();
+        if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
+            return OptionalInt.empty();
+        }
+        CharSequence[] labels = suggestedFolderNames.getLabels();
+        return IntStream.range(0, labels.length)
+                .filter(index -> !isEmpty(labels[index])
+                        && newLabel.equalsIgnoreCase(
+                        labels[index].toString()))
+                .sequential()
+                .findFirst();
+    }
+
+    /**
+     * Returns {@link FromState} based on current {@link #title}.
+     */
+    public LauncherAtom.FromState getFromLabelState() {
+        switch (getLabelState()){
+            case EMPTY:
+                return LauncherAtom.FromState.FROM_EMPTY;
+            case MANUAL:
+                return LauncherAtom.FromState.FROM_CUSTOM;
+            case SUGGESTED:
+                return LauncherAtom.FromState.FROM_SUGGESTED;
+            case UNLABELED:
+            default:
+                return LauncherAtom.FromState.FROM_STATE_UNSPECIFIED;
+        }
+    }
+
+    /**
+     * Returns {@link ToState} based on current {@link #title}.
+     */
+    public LauncherAtom.ToState getToLabelState() {
+        if (title == null) {
+            return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
+        }
+
+        if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            return title.length() > 0
+                    ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
+        }
+
+        // TODO: if suggestedFolderNames is null then it infrastructure issue, not
+        // ranking issue. We should log these appropriately.
+        if (suggestedFolderNames == null || !suggestedFolderNames.hasSuggestions()) {
+            return title.length() > 0
+                    ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
+        }
+
+        boolean hasValidPrimary = suggestedFolderNames != null && suggestedFolderNames.hasPrimary();
+        if (title.length() == 0) {
+            return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+        }
+
+        OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
+        if (!accepted_suggestion_index.isPresent()) {
+            return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
+                    : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+        }
+
+        switch (accepted_suggestion_index.getAsInt()) {
+            case 0:
+                return LauncherAtom.ToState.TO_SUGGESTION0;
+            case 1:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
+            case 2:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
+            case 3:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
+            default:
+                // fall through
+        }
+        return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
+    }
+
+    /**
+     * Returns {@link LauncherLogProto.LauncherEvent} to log current folder label info.
+     *
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent(FromState fromState,
+            ToState toState) {
+        return LauncherLogProto.LauncherEvent.newBuilder()
+                .setAction(LauncherLogProto.Action
+                        .newBuilder()
+                        .setType(LauncherLogProto.Action.Type.SOFT_KEYBOARD))
+                .addSrcTarget(Target
+                        .newBuilder()
+                        .setType(Target.Type.ITEM)
+                        .setItemType(LauncherLogProto.ItemType.EDITTEXT)
+                        .setFromFolderLabelState(convertFolderLabelState(fromState))
+                        .setToFolderLabelState(convertFolderLabelState(toState)))
+                .addSrcTarget(Target.newBuilder()
+                        .setType(Target.Type.CONTAINER)
+                        .setContainerType(LauncherLogProto.ContainerType.FOLDER)
+                        .setPageIndex(screenId)
+                        .setGridX(cellX)
+                        .setGridY(cellY)
+                        .setCardinality(contents.size()))
+                .addSrcTarget(newParentContainerTarget())
+                .build();
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private Target.Builder newParentContainerTarget() {
+        Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
+        switch (container) {
+            case CONTAINER_HOTSEAT:
+                return builder.setContainerType(LauncherLogProto.ContainerType.HOTSEAT);
+            case CONTAINER_DESKTOP:
+                return builder.setContainerType(LauncherLogProto.ContainerType.WORKSPACE);
+            default:
+                throw new AssertionError(String
+                        .format("Expected container to be either %s or %s but found %s.",
+                                CONTAINER_HOTSEAT,
+                                CONTAINER_DESKTOP,
+                                container));
+        }
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private static FromFolderLabelState convertFolderLabelState(FromState fromState) {
+        switch (fromState) {
+            case FROM_EMPTY:
+                return FROM_EMPTY;
+            case FROM_SUGGESTED:
+                return FROM_SUGGESTED;
+            case FROM_CUSTOM:
+                return FROM_CUSTOM;
+            default:
+                return FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
+        }
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private static ToFolderLabelState convertFolderLabelState(ToState toState) {
+        switch (toState) {
+            case UNCHANGED:
+                return ToFolderLabelState.UNCHANGED;
+            case TO_SUGGESTION0:
+                return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION1_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION1_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
+            case TO_SUGGESTION2_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION2_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
+            case TO_SUGGESTION3_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION3_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
+            case TO_EMPTY_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY;
+            case TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+            case TO_EMPTY_WITH_EMPTY_SUGGESTIONS:
+                return ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
+            case TO_EMPTY_WITH_SUGGESTIONS_DISABLED:
+                return ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
+            case TO_CUSTOM_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY;
+            case TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+            case TO_CUSTOM_WITH_EMPTY_SUGGESTIONS:
+                return ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS;
+            case TO_CUSTOM_WITH_SUGGESTIONS_DISABLED:
+                return ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED;
+            default:
+                return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
new file mode 100644
index 0000000..0d3ddad
--- /dev/null
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model.data;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+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_SEARCH_RESULTS;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SETTINGS;
+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.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+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.LauncherSettings.Favorites.ITEM_TYPE_TASK;
+import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.CONTAINER_NOT_SET;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.AllAppsContainer;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.PredictionContainer;
+import com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
+import com.android.launcher3.logger.LauncherAtom.SettingsContainer;
+import com.android.launcher3.logger.LauncherAtom.ShortcutsContainer;
+import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer;
+import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.util.ContentWriter;
+
+import java.util.Optional;
+
+/**
+ * Represents an item in the launcher.
+ */
+public class ItemInfo {
+
+    public static final boolean DEBUG = true;
+    public static final int NO_ID = -1;
+
+    /**
+     * The id in the settings database for this item
+     */
+    public int id = NO_ID;
+
+    /**
+     * One of {@link Favorites#ITEM_TYPE_APPLICATION},
+     * {@link Favorites#ITEM_TYPE_SHORTCUT},
+     * {@link Favorites#ITEM_TYPE_DEEP_SHORTCUT}
+     * {@link Favorites#ITEM_TYPE_FOLDER},
+     * {@link Favorites#ITEM_TYPE_APPWIDGET} or
+     * {@link Favorites#ITEM_TYPE_CUSTOM_APPWIDGET}.
+     */
+    public int itemType;
+
+    /**
+     * The id of the container that holds this item. For the desktop, this will be
+     * {@link Favorites#CONTAINER_DESKTOP}. For the all applications folder it
+     * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders
+     * it will be the id of the folder.
+     */
+    public int container = NO_ID;
+
+    /**
+     * Indicates the screen in which the shortcut appears if the container types is
+     * {@link Favorites#CONTAINER_DESKTOP}. (i.e., ignore if the container type is
+     * {@link Favorites#CONTAINER_HOTSEAT})
+     */
+    public int screenId = -1;
+
+    /**
+     * Indicates the X position of the associated cell.
+     */
+    public int cellX = -1;
+
+    /**
+     * Indicates the Y position of the associated cell.
+     */
+    public int cellY = -1;
+
+    /**
+     * Indicates the X cell span.
+     */
+    public int spanX = 1;
+
+    /**
+     * Indicates the Y cell span.
+     */
+    public int spanY = 1;
+
+    /**
+     * Indicates the minimum X cell span.
+     */
+    public int minSpanX = 1;
+
+    /**
+     * Indicates the minimum Y cell span.
+     */
+    public int minSpanY = 1;
+
+    /**
+     * Indicates the position in an ordered list.
+     */
+    public int rank = 0;
+
+    /**
+     * Title of the item
+     */
+    public CharSequence title;
+
+    /**
+     * Content description of the item.
+     */
+    public CharSequence contentDescription;
+
+    /**
+     * When the instance is created using {@link #copyFrom}, this field is used to keep track of
+     * original {@link ComponentName}.
+     */
+    private ComponentName mComponentName;
+
+    public UserHandle user;
+
+    public ItemInfo() {
+        user = Process.myUserHandle();
+    }
+
+    protected ItemInfo(ItemInfo info) {
+        copyFrom(info);
+    }
+
+    public void copyFrom(ItemInfo info) {
+        id = info.id;
+        cellX = info.cellX;
+        cellY = info.cellY;
+        spanX = info.spanX;
+        spanY = info.spanY;
+        rank = info.rank;
+        screenId = info.screenId;
+        itemType = info.itemType;
+        container = info.container;
+        user = info.user;
+        contentDescription = info.contentDescription;
+        mComponentName = info.getTargetComponent();
+    }
+
+    public Intent getIntent() {
+        return null;
+    }
+
+    @Nullable
+    public ComponentName getTargetComponent() {
+        return Optional.ofNullable(getIntent()).map(Intent::getComponent).orElse(mComponentName);
+    }
+
+    public void writeToValues(ContentWriter writer) {
+        writer.put(LauncherSettings.Favorites.ITEM_TYPE, itemType)
+                .put(LauncherSettings.Favorites.CONTAINER, container)
+                .put(LauncherSettings.Favorites.SCREEN, screenId)
+                .put(LauncherSettings.Favorites.CELLX, cellX)
+                .put(LauncherSettings.Favorites.CELLY, cellY)
+                .put(LauncherSettings.Favorites.SPANX, spanX)
+                .put(LauncherSettings.Favorites.SPANY, spanY)
+                .put(LauncherSettings.Favorites.RANK, rank);
+    }
+
+    public void readFromValues(ContentValues values) {
+        itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
+        container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
+        screenId = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
+        cellX = values.getAsInteger(LauncherSettings.Favorites.CELLX);
+        cellY = values.getAsInteger(LauncherSettings.Favorites.CELLY);
+        spanX = values.getAsInteger(LauncherSettings.Favorites.SPANX);
+        spanY = values.getAsInteger(LauncherSettings.Favorites.SPANY);
+        rank = values.getAsInteger(LauncherSettings.Favorites.RANK);
+    }
+
+    /**
+     * Write the fields of this item to the DB
+     */
+    public void onAddToDatabase(ContentWriter writer) {
+        if (screenId == Workspace.EXTRA_EMPTY_SCREEN_ID) {
+            // We should never persist an item on the extra empty screen.
+            throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID");
+        }
+
+        writeToValues(writer);
+        writer.put(LauncherSettings.Favorites.PROFILE_ID, user);
+    }
+
+    @Override
+    public final String toString() {
+        return getClass().getSimpleName() + "(" + dumpProperties() + ")";
+    }
+
+    protected String dumpProperties() {
+        return "id=" + id
+                + " type=" + LauncherSettings.Favorites.itemTypeToString(itemType)
+                + " container=" + LauncherSettings.Favorites.containerToString(container)
+                + " targetComponent=" + getTargetComponent()
+                + " screen=" + screenId
+                + " cell(" + cellX + "," + cellY + ")"
+                + " span(" + spanX + "," + spanY + ")"
+                + " minSpan(" + minSpanX + "," + minSpanY + ")"
+                + " rank=" + rank
+                + " user=" + user
+                + " title=" + title;
+    }
+
+    /**
+     * Whether this item is disabled.
+     */
+    public boolean isDisabled() {
+        return false;
+    }
+
+    public int getViewId() {
+        // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
+        // This cast is safe as long as the id < 0x00FFFFFF
+        // Since we jail all the dynamically generated views, there should be no clashes
+        // with any other views.
+        return id;
+    }
+
+    /**
+     * Returns if an Item is a predicted item
+     */
+    public boolean isPredictedItem() {
+        return container == CONTAINER_HOTSEAT_PREDICTION || container == CONTAINER_PREDICTION;
+    }
+
+    /**
+     * Can be overridden by inherited classes to fill in {@link LauncherAtom.ItemInfo}
+     */
+    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
+    }
+
+    /**
+     * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
+     */
+    public LauncherAtom.ItemInfo buildProto() {
+        return buildProto(null);
+    }
+
+    /**
+     * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
+     */
+    public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
+        LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder();
+        Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
+        switch (itemType) {
+            case ITEM_TYPE_APPLICATION:
+                itemBuilder
+                        .setApplication(nullableComponent
+                                .map(component -> LauncherAtom.Application.newBuilder()
+                                        .setComponentName(component.flattenToShortString())
+                                        .setPackageName(component.getPackageName()))
+                                .orElse(LauncherAtom.Application.newBuilder()));
+                break;
+            case ITEM_TYPE_DEEP_SHORTCUT:
+            case ITEM_TYPE_SHORTCUT:
+                itemBuilder
+                        .setShortcut(nullableComponent
+                                .map(component -> LauncherAtom.Shortcut.newBuilder()
+                                        .setShortcutName(component.flattenToShortString()))
+                                .orElse(LauncherAtom.Shortcut.newBuilder()));
+                break;
+            case ITEM_TYPE_APPWIDGET:
+                itemBuilder
+                        .setWidget(nullableComponent
+                                .map(component -> LauncherAtom.Widget.newBuilder()
+                                        .setComponentName(component.flattenToShortString())
+                                        .setPackageName(component.getPackageName()))
+                                .orElse(LauncherAtom.Widget.newBuilder())
+                                .setSpanX(spanX)
+                                .setSpanY(spanY));
+                break;
+            case ITEM_TYPE_TASK:
+                itemBuilder
+                        .setTask(LauncherAtom.Task.newBuilder()
+                                .setComponentName(getTargetComponent().flattenToShortString())
+                                .setIndex(screenId));
+                break;
+            default:
+                break;
+        }
+        if (fInfo != null) {
+            LauncherAtom.FolderContainer.Builder folderBuilder =
+                    LauncherAtom.FolderContainer.newBuilder();
+            folderBuilder.setGridX(cellX).setGridY(cellY).setPageIndex(screenId);
+
+            switch (fInfo.container) {
+                case CONTAINER_HOTSEAT:
+                case CONTAINER_HOTSEAT_PREDICTION:
+                    folderBuilder.setHotseat(LauncherAtom.HotseatContainer.newBuilder()
+                            .setIndex(fInfo.screenId));
+                    break;
+                case CONTAINER_DESKTOP:
+                    folderBuilder.setWorkspace(LauncherAtom.WorkspaceContainer.newBuilder()
+                            .setPageIndex(fInfo.screenId)
+                            .setGridX(fInfo.cellX).setGridY(fInfo.cellY));
+                    break;
+            }
+            itemBuilder.setContainerInfo(ContainerInfo.newBuilder().setFolder(folderBuilder));
+        } else {
+            ContainerInfo containerInfo = getContainerInfo();
+            if (!containerInfo.getContainerCase().equals(CONTAINER_NOT_SET)) {
+                itemBuilder.setContainerInfo(containerInfo);
+            }
+        }
+        return itemBuilder.build();
+    }
+
+    LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() {
+        LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
+        itemBuilder.setIsWork(user != Process.myUserHandle());
+        return itemBuilder;
+    }
+
+    protected ContainerInfo getContainerInfo() {
+        switch (container) {
+            case CONTAINER_HOTSEAT:
+                return ContainerInfo.newBuilder()
+                        .setHotseat(LauncherAtom.HotseatContainer.newBuilder().setIndex(screenId))
+                        .build();
+            case CONTAINER_HOTSEAT_PREDICTION:
+                return ContainerInfo.newBuilder().setPredictedHotseatContainer(
+                        LauncherAtom.PredictedHotseatContainer.newBuilder().setIndex(screenId))
+                        .build();
+            case CONTAINER_DESKTOP:
+                return ContainerInfo.newBuilder()
+                        .setWorkspace(
+                                LauncherAtom.WorkspaceContainer.newBuilder()
+                                        .setGridX(cellX)
+                                        .setGridY(cellY)
+                                        .setPageIndex(screenId))
+                        .build();
+            case CONTAINER_ALL_APPS:
+                return ContainerInfo.newBuilder()
+                        .setAllAppsContainer(
+                                AllAppsContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_WIDGETS_TRAY:
+                return ContainerInfo.newBuilder()
+                        .setWidgetsContainer(
+                                LauncherAtom.WidgetsContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_PREDICTION:
+                return ContainerInfo.newBuilder()
+                        .setPredictionContainer(PredictionContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_SEARCH_RESULTS:
+                return ContainerInfo.newBuilder()
+                        .setSearchResultContainer(SearchResultContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_SHORTCUTS:
+                return ContainerInfo.newBuilder()
+                        .setShortcutsContainer(ShortcutsContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_SETTINGS:
+                return ContainerInfo.newBuilder()
+                        .setSettingsContainer(SettingsContainer.getDefaultInstance())
+                        .build();
+            case CONTAINER_TASKSWITCHER:
+                return ContainerInfo.newBuilder()
+                        .setTaskSwitcherContainer(TaskSwitcherContainer.getDefaultInstance())
+                        .build();
+
+        }
+        return ContainerInfo.getDefaultInstance();
+    }
+
+    /**
+     * Returns shallow copy of the object.
+     */
+    public ItemInfo makeShallowCopy() {
+        ItemInfo itemInfo = new ItemInfo();
+        itemInfo.copyFrom(this);
+        return itemInfo;
+    }
+
+    /**
+     * Sets the title of the item and writes to DB model if needed.
+     */
+    public void setTitle(CharSequence title, ModelWriter modelWriter) {
+        this.title = title;
+    }
+}
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
similarity index 81%
rename from src/com/android/launcher3/ItemInfoWithIcon.java
rename to src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 1550bb0..d95f94f 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -14,11 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
-
-import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
-
-import android.graphics.Bitmap;
+package com.android.launcher3.model.data;
 
 import com.android.launcher3.icons.BitmapInfo;
 
@@ -30,14 +26,9 @@
     public static final String TAG = "ItemInfoDebug";
 
     /**
-     * A bitmap version of the application icon.
+     * The bitmap for the application icon
      */
-    public Bitmap iconBitmap;
-
-    /**
-     * Dominant color in the {@link #iconBitmap}.
-     */
-    public int iconColor;
+    public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
 
     /**
      * Indicates that the icon is disabled due to safe mode restrictions.
@@ -69,9 +60,9 @@
      */
     public static final int FLAG_DISABLED_LOCKED_USER = 1 << 5;
 
-    public static final int FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE |
-            FLAG_DISABLED_NOT_AVAILABLE | FLAG_DISABLED_SUSPENDED |
-            FLAG_DISABLED_QUIET_USER | FLAG_DISABLED_BY_PUBLISHER | FLAG_DISABLED_LOCKED_USER;
+    public static final int FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE
+            | FLAG_DISABLED_NOT_AVAILABLE | FLAG_DISABLED_SUSPENDED
+            | FLAG_DISABLED_QUIET_USER | FLAG_DISABLED_BY_PUBLISHER | FLAG_DISABLED_LOCKED_USER;
 
     /**
      * The item points to a system app.
@@ -106,8 +97,7 @@
 
     protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
-        iconBitmap = info.iconBitmap;
-        iconColor = info.iconColor;
+        bitmap = info.bitmap;
         runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
@@ -120,12 +110,7 @@
      * Indicates whether we're using a low res icon
      */
     public boolean usingLowResIcon() {
-        return iconBitmap == LOW_RES_ICON;
-    }
-
-    public void applyFrom(BitmapInfo info) {
-        iconBitmap = info.icon;
-        iconColor = info.color;
+        return bitmap.isLowRes();
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
similarity index 83%
rename from src/com/android/launcher3/LauncherAppWidgetInfo.java
rename to src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index b824301..b0d19a6 100644
--- a/src/com/android/launcher3/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -14,14 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
 
 import android.appwidget.AppWidgetHostView;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Process;
 
-import com.android.launcher3.model.PackageItemInfo;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AppWidgetResizeFrame;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.util.ContentWriter;
 
 /**
@@ -139,6 +144,12 @@
         return appWidgetId <= CUSTOM_WIDGET_ID;
     }
 
+    @Nullable
+    @Override
+    public ComponentName getTargetComponent() {
+        return providerName;
+    }
+
     @Override
     public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
@@ -153,7 +164,7 @@
      * When we bind the widget, we should notify the widget that the size has changed if we have not
      * done so already (only really for default workspace widgets).
      */
-    void onBindAppWidget(Launcher launcher, AppWidgetHostView hostView) {
+    public void onBindAppWidget(Launcher launcher, AppWidgetHostView hostView) {
         if (!mHasNotifiedInitialWidgetSizeChanged) {
             AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY);
             mHasNotifiedInitialWidgetSizeChanged = true;
@@ -162,12 +173,14 @@
 
     @Override
     protected String dumpProperties() {
-        return super.dumpProperties() + " appWidgetId=" + appWidgetId;
+        return super.dumpProperties()
+                + " providerName=" + providerName
+                + " appWidgetId=" + appWidgetId;
     }
 
     public final boolean isWidgetIdAllocated() {
-        return (restoreStatus & FLAG_ID_NOT_VALID) == 0 ||
-                (restoreStatus & FLAG_ID_ALLOCATED) == FLAG_ID_ALLOCATED;
+        return (restoreStatus & FLAG_ID_NOT_VALID) == 0
+                || (restoreStatus & FLAG_ID_ALLOCATED) == FLAG_ID_ALLOCATED;
     }
 
     public final boolean hasRestoreFlag(int flag) {
@@ -182,4 +195,13 @@
     public final boolean hasOptionFlag(int option) {
         return (options & option) != 0;
     }
+
+    @Override
+    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
+        builder.setWidget(LauncherAtom.Widget.newBuilder()
+                .setSpanX(spanX)
+                .setSpanY(spanY)
+                .setComponentName(providerName.toString())
+                .setPackageName(providerName.getPackageName()));
+    }
 }
diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/data/PackageItemInfo.java
similarity index 77%
rename from src/com/android/launcher3/model/PackageItemInfo.java
rename to src/com/android/launcher3/model/data/PackageItemInfo.java
index 3ef48cd..b70d0d4 100644
--- a/src/com/android/launcher3/model/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/data/PackageItemInfo.java
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.model;
+package com.android.launcher3.model.data;
 
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherSettings;
 
+import java.util.Objects;
+
 /**
  * Represents a {@link Package} in the widget tray section.
  */
@@ -48,4 +49,17 @@
     public PackageItemInfo clone() {
         return new PackageItemInfo(this);
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PackageItemInfo that = (PackageItemInfo) o;
+        return Objects.equals(packageName, that.packageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageName);
+    }
 }
diff --git a/src/com/android/launcher3/PromiseAppInfo.java b/src/com/android/launcher3/model/data/PromiseAppInfo.java
similarity index 90%
rename from src/com/android/launcher3/PromiseAppInfo.java
rename to src/com/android/launcher3/model/data/PromiseAppInfo.java
index 4ad0b3d..b6231ed 100644
--- a/src/com/android/launcher3/PromiseAppInfo.java
+++ b/src/com/android/launcher3/model/data/PromiseAppInfo.java
@@ -14,21 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
 
 import android.content.Context;
 import android.content.Intent;
 
-import com.android.launcher3.compat.PackageInstallerCompat;
-import com.android.launcher3.util.PackageManagerHelper;
-
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.PackageManagerHelper;
+
 public class PromiseAppInfo extends AppInfo {
 
     public int level = 0;
 
-    public PromiseAppInfo(@NonNull PackageInstallerCompat.PackageInstallInfo installInfo) {
+    public PromiseAppInfo(@NonNull PackageInstallInfo installInfo) {
         componentName = installInfo.componentName;
         intent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_LAUNCHER)
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
similarity index 92%
rename from src/com/android/launcher3/WorkspaceItemInfo.java
rename to src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index 82fc600..1e1d093 100644
--- a/src/com/android/launcher3/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
 
 import android.app.Person;
 import android.content.ComponentName;
@@ -25,10 +25,12 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.Arrays;
@@ -120,7 +122,7 @@
     public WorkspaceItemInfo(AppInfo info) {
         super(info);
         title = Utilities.trim(info.title);
-        intent = new Intent(info.intent);
+        intent = new Intent(info.getIntent());
     }
 
     /**
@@ -140,7 +142,7 @@
                 .put(Favorites.RESTORED, status);
 
         if (!usingLowResIcon()) {
-            writer.putIcon(iconBitmap, user);
+            writer.putIcon(bitmap, user);
         }
         if (iconResource != null) {
             writer.put(Favorites.ICON_PACKAGE, iconResource.packageName)
@@ -192,15 +194,15 @@
         }
         disabledMessage = shortcutInfo.getDisabledMessage();
 
-        Person[] persons = UiFactory.getPersons(shortcutInfo);
+        Person[] persons = ApiWrapper.getPersons(shortcutInfo);
         personKeys = persons.length == 0 ? Utilities.EMPTY_STRING_ARRAY
             : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);
     }
 
     /** Returns the WorkspaceItemInfo id associated with the deep shortcut. */
     public String getDeepShortcutId() {
-        return itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT ?
-                getIntent().getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) : null;
+        return itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                ? getIntent().getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) : null;
     }
 
     @NonNull
@@ -211,8 +213,8 @@
     @Override
     public ComponentName getTargetComponent() {
         ComponentName cn = super.getTargetComponent();
-        if (cn == null && (itemType == Favorites.ITEM_TYPE_SHORTCUT
-                || hasStatusFlag(FLAG_SUPPORTS_WEB_UI|FLAG_AUTOINSTALL_ICON|FLAG_RESTORED_ICON))) {
+        if (cn == null && (itemType == Favorites.ITEM_TYPE_SHORTCUT || hasStatusFlag(
+                FLAG_SUPPORTS_WEB_UI | FLAG_AUTOINSTALL_ICON | FLAG_RESTORED_ICON))) {
             // Legacy shortcuts and promise icons with web UI may not have a componentName but just
             // a packageName. In that case create a empty componentName instead of adding additional
             // check everywhere.
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
index c7de5b0..fd3d41a 100644
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -80,17 +80,28 @@
         int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
         mIconLayoutParams = new LayoutParams(iconSize, iconSize);
         mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
-        // Compute margin start for each icon such that the icons between the first one
-        // and the ellipsis are evenly spaced out.
+        setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
+        mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
+    }
+
+
+    /**
+     * Compute margin start for each icon such that the icons between the first one and the ellipsis
+     * are evenly spaced out.
+     */
+    public void setWidth(int width) {
+        if (getLayoutParams() != null) {
+            getLayoutParams().width = width;
+        }
+        Resources res = getResources();
+        int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
+
         int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
         int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
                 + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
-        int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width);
-        int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
+        int availableIconRowSpace = width - paddingEnd - ellipsisSpace
                 - iconSize * MAX_FOOTER_NOTIFICATIONS;
         mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
-
-        mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
     }
 
     @Override
diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index e5525b2..835f72d 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_LAUNCH_TAP;
+
 import android.app.ActivityOptions;
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -32,6 +34,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
 
 /**
@@ -51,6 +54,7 @@
     public final boolean autoCancel;
     public final boolean dismissable;
 
+    private final ItemInfo mItemInfo;
     private Drawable mIconDrawable;
     private int mIconColor;
     private boolean mIsIconLarge;
@@ -58,7 +62,8 @@
     /**
      * Extracts the data that we need from the StatusBarNotification.
      */
-    public NotificationInfo(Context context, StatusBarNotification statusBarNotification) {
+    public NotificationInfo(Context context, StatusBarNotification statusBarNotification,
+            ItemInfo itemInfo) {
         packageUserKey = PackageUserKey.fromNotification(statusBarNotification);
         notificationKey = statusBarNotification.getKey();
         Notification notification = statusBarNotification.getNotification();
@@ -88,6 +93,7 @@
         intent = notification.contentIntent;
         autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
         dismissable = (notification.flags & Notification.FLAG_ONGOING_EVENT) == 0;
+        this.mItemInfo = itemInfo;
     }
 
     @Override
@@ -101,6 +107,8 @@
         try {
             intent.send(null, 0, null, null, null, null, activityOptions);
             launcher.getUserEventDispatcher().logNotificationLaunch(view, intent);
+            launcher.getStatsLogManager().logger().withItemInfo(mItemInfo)
+                    .log(LAUNCHER_NOTIFICATION_LAUNCH_TAP);
         } catch (PendingIntent.CanceledException e) {
             e.printStackTrace();
         }
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 021fb30..0320aa3 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -86,6 +86,13 @@
         }
     }
 
+    /**
+     * Sets width for notification footer and spaces out items evenly
+     */
+    public void setFooterWidth(int footerWidth) {
+        mFooter.setWidth(footerWidth);
+    }
+
     public void removeFooter() {
         if (mContainer.indexOfChild(mFooter) >= 0) {
             mContainer.removeView(mFooter);
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 10378ee..059ad18 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.notification;
 
+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;
 
@@ -32,9 +33,10 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.SecureSettingsObserver;
 
@@ -44,6 +46,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * A {@link NotificationListenerService} that sends updates to its
@@ -59,16 +62,17 @@
     private static final int MSG_NOTIFICATION_POSTED = 1;
     private static final int MSG_NOTIFICATION_REMOVED = 2;
     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
+    private static final int MSG_CANCEL_NOTIFICATION = 4;
+    private static final int MSG_RANKING_UPDATE = 5;
 
     private static NotificationListener sNotificationListenerInstance = null;
     private static NotificationsChangedListener sNotificationsChangedListener;
-    private static StatusBarNotificationsChangedListener sStatusBarNotificationsChangedListener;
     private static boolean sIsConnected;
-    private static boolean sIsCreated;
 
     private final Handler mWorkerHandler;
     private final Handler mUiHandler;
     private final Ranking mTempRanking = new Ranking();
+
     /** Maps groupKey's to the corresponding group of notifications. */
     private final Map<String, NotificationGroup> mNotificationGroupMap = new HashMap<>();
     /** Maps keys to their corresponding current group key */
@@ -79,85 +83,12 @@
 
     private SecureSettingsObserver mNotificationDotsObserver;
 
-    private final Handler.Callback mWorkerCallback = new Handler.Callback() {
-        @Override
-        public boolean handleMessage(Message message) {
-            switch (message.what) {
-                case MSG_NOTIFICATION_POSTED:
-                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
-                    break;
-                case MSG_NOTIFICATION_REMOVED:
-                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
-                    break;
-                case MSG_NOTIFICATION_FULL_REFRESH:
-                    List<StatusBarNotification> activeNotifications;
-                    if (sIsConnected) {
-                        try {
-                            activeNotifications = filterNotifications(getActiveNotifications());
-                        } catch (SecurityException ex) {
-                            Log.e(TAG, "SecurityException: failed to fetch notifications");
-                            activeNotifications = new ArrayList<StatusBarNotification>();
-
-                        }
-                    } else {
-                        activeNotifications = new ArrayList<StatusBarNotification>();
-                    }
-
-                    mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
-                    break;
-            }
-            return true;
-        }
-    };
-
-    private final Handler.Callback mUiCallback = new Handler.Callback() {
-        @Override
-        public boolean handleMessage(Message message) {
-            switch (message.what) {
-                case MSG_NOTIFICATION_POSTED:
-                    if (sNotificationsChangedListener != null) {
-                        NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
-                        sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
-                                msg.notificationKey, msg.shouldBeFilteredOut);
-                    }
-                    break;
-                case MSG_NOTIFICATION_REMOVED:
-                    if (sNotificationsChangedListener != null) {
-                        Pair<PackageUserKey, NotificationKeyData> pair
-                                = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
-                        sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
-                    }
-                    break;
-                case MSG_NOTIFICATION_FULL_REFRESH:
-                    if (sNotificationsChangedListener != null) {
-                        sNotificationsChangedListener.onNotificationFullRefresh(
-                                (List<StatusBarNotification>) message.obj);
-                    }
-                    break;
-            }
-            return true;
-        }
-    };
-
     public NotificationListener() {
-        super();
-        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), mWorkerCallback);
-        mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
+        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
+        mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         sNotificationListenerInstance = this;
     }
 
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        sIsCreated = true;
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        sIsCreated = false;
-    }
-
     public static @Nullable NotificationListener getInstanceIfConnected() {
         return sIsConnected ? sNotificationListenerInstance : null;
     }
@@ -168,25 +99,107 @@
         NotificationListener notificationListener = getInstanceIfConnected();
         if (notificationListener != null) {
             notificationListener.onNotificationFullRefresh();
-        } else if (!sIsCreated && sNotificationsChangedListener != null) {
+        } else {
             // User turned off dots globally, so we unbound this service;
             // tell the listener that there are no notifications to remove dots.
-            sNotificationsChangedListener.onNotificationFullRefresh(
-                    Collections.<StatusBarNotification>emptyList());
+            MODEL_EXECUTOR.submit(() -> MAIN_EXECUTOR.submit(() ->
+                            listener.onNotificationFullRefresh(Collections.emptyList())));
         }
     }
 
-    public static void setStatusBarNotificationsChangedListener
-            (StatusBarNotificationsChangedListener listener) {
-        sStatusBarNotificationsChangedListener = listener;
-    }
-
     public static void removeNotificationsChangedListener() {
         sNotificationsChangedListener = null;
     }
 
-    public static void removeStatusBarNotificationsChangedListener() {
-        sStatusBarNotificationsChangedListener = null;
+    private boolean handleWorkerMessage(Message message) {
+        switch (message.what) {
+            case MSG_NOTIFICATION_POSTED: {
+                StatusBarNotification sbn = (StatusBarNotification) message.obj;
+                mUiHandler.obtainMessage(notificationIsValidForUI(sbn)
+                                ? MSG_NOTIFICATION_POSTED : MSG_NOTIFICATION_REMOVED,
+                        toKeyPair(sbn)).sendToTarget();
+                return true;
+            }
+            case MSG_NOTIFICATION_REMOVED: {
+                StatusBarNotification sbn = (StatusBarNotification) message.obj;
+                mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED,
+                        toKeyPair(sbn)).sendToTarget();
+
+                NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
+                String key = sbn.getKey();
+                if (notificationGroup != null) {
+                    notificationGroup.removeChildKey(key);
+                    if (notificationGroup.isEmpty()) {
+                        if (key.equals(mLastKeyDismissedByLauncher)) {
+                            // Only cancel the group notification if launcher dismissed the
+                            // last child.
+                            cancelNotification(notificationGroup.getGroupSummaryKey());
+                        }
+                        mNotificationGroupMap.remove(sbn.getGroupKey());
+                    }
+                }
+                if (key.equals(mLastKeyDismissedByLauncher)) {
+                    mLastKeyDismissedByLauncher = null;
+                }
+                return true;
+            }
+            case MSG_NOTIFICATION_FULL_REFRESH:
+                List<StatusBarNotification> activeNotifications = null;
+                if (sIsConnected) {
+                    try {
+                        activeNotifications = Arrays.stream(getActiveNotifications())
+                                .filter(this::notificationIsValidForUI)
+                                .collect(Collectors.toList());
+                    } catch (SecurityException ex) {
+                        Log.e(TAG, "SecurityException: failed to fetch notifications");
+                        activeNotifications = new ArrayList<>();
+                    }
+                } else {
+                    activeNotifications = new ArrayList<>();
+                }
+
+                mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
+                return true;
+            case MSG_CANCEL_NOTIFICATION: {
+                mLastKeyDismissedByLauncher = (String) message.obj;
+                cancelNotification(mLastKeyDismissedByLauncher);
+                return true;
+            }
+            case MSG_RANKING_UPDATE: {
+                String[] keys = ((RankingMap) message.obj).getOrderedKeys();
+                for (StatusBarNotification sbn : getActiveNotifications(keys)) {
+                    updateGroupKeyIfNecessary(sbn);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean handleUiMessage(Message message) {
+        switch (message.what) {
+            case MSG_NOTIFICATION_POSTED:
+                if (sNotificationsChangedListener != null) {
+                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
+                    sNotificationsChangedListener.onNotificationPosted(
+                            msg.first, msg.second);
+                }
+                break;
+            case MSG_NOTIFICATION_REMOVED:
+                if (sNotificationsChangedListener != null) {
+                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
+                    sNotificationsChangedListener.onNotificationRemoved(
+                            msg.first, msg.second);
+                }
+                break;
+            case MSG_NOTIFICATION_FULL_REFRESH:
+                if (sNotificationsChangedListener != null) {
+                    sNotificationsChangedListener.onNotificationFullRefresh(
+                            (List<StatusBarNotification>) message.obj);
+                }
+                break;
+        }
+        return true;
     }
 
     @Override
@@ -217,84 +230,37 @@
         super.onListenerDisconnected();
         sIsConnected = false;
         mNotificationDotsObserver.unregister();
+        onNotificationFullRefresh();
     }
 
     @Override
     public void onNotificationPosted(final StatusBarNotification sbn) {
-        super.onNotificationPosted(sbn);
-        if (sbn == null) {
-            // There is a bug in platform where we can get a null notification; just ignore it.
-            return;
-        }
-        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
-            .sendToTarget();
-        if (sStatusBarNotificationsChangedListener != null) {
-            sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
-        }
-    }
-
-    /**
-     * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
-     */
-    private class NotificationPostedMsg {
-        final PackageUserKey packageUserKey;
-        final NotificationKeyData notificationKey;
-        final boolean shouldBeFilteredOut;
-
-        NotificationPostedMsg(StatusBarNotification sbn) {
-            packageUserKey = PackageUserKey.fromNotification(sbn);
-            notificationKey = NotificationKeyData.fromNotification(sbn);
-            shouldBeFilteredOut = shouldBeFilteredOut(sbn);
+        if (sbn != null) {
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, sbn).sendToTarget();
         }
     }
 
     @Override
     public void onNotificationRemoved(final StatusBarNotification sbn) {
-        super.onNotificationRemoved(sbn);
-        if (sbn == null) {
-            // There is a bug in platform where we can get a null notification; just ignore it.
-            return;
+        if (sbn != null) {
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, sbn).sendToTarget();
         }
-        Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
-            = new Pair<>(PackageUserKey.fromNotification(sbn),
-            NotificationKeyData.fromNotification(sbn));
-        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
-            .sendToTarget();
-        if (sStatusBarNotificationsChangedListener != null) {
-            sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
-        }
-
-        NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
-        String key = sbn.getKey();
-        if (notificationGroup != null) {
-            notificationGroup.removeChildKey(key);
-            if (notificationGroup.isEmpty()) {
-                if (key.equals(mLastKeyDismissedByLauncher)) {
-                    // Only cancel the group notification if launcher dismissed the last child.
-                    cancelNotification(notificationGroup.getGroupSummaryKey());
-                }
-                mNotificationGroupMap.remove(sbn.getGroupKey());
-            }
-        }
-        if (key.equals(mLastKeyDismissedByLauncher)) {
-            mLastKeyDismissedByLauncher = null;
-        }
-    }
-
-    public void cancelNotificationFromLauncher(String key) {
-        mLastKeyDismissedByLauncher = key;
-        cancelNotification(key);
     }
 
     @Override
     public void onNotificationRankingUpdate(RankingMap rankingMap) {
-        super.onNotificationRankingUpdate(rankingMap);
-        String[] keys = rankingMap.getOrderedKeys();
-        for (StatusBarNotification sbn : getActiveNotifications(keys)) {
-            updateGroupKeyIfNecessary(sbn);
-        }
+        mWorkerHandler.obtainMessage(MSG_RANKING_UPDATE, rankingMap).sendToTarget();
     }
 
+    /**
+     * Cancels a notification
+     */
+    @AnyThread
+    public void cancelNotificationFromLauncher(String key) {
+        mWorkerHandler.obtainMessage(MSG_CANCEL_NOTIFICATION, key).sendToTarget();
+    }
+
+    @WorkerThread
     private void updateGroupKeyIfNecessary(StatusBarNotification sbn) {
         String childKey = sbn.getKey();
         String oldGroupKey = mNotificationGroupKeyMap.get(childKey);
@@ -328,53 +294,33 @@
         }
     }
 
-    /** This makes a potentially expensive binder call and should be run on a background thread. */
+    /**
+     * This makes a potentially expensive binder call and should be run on a background thread.
+     */
+    @WorkerThread
     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
-        StatusBarNotification[] notifications = NotificationListener.this
-                .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
-                        .toArray(new String[keys.size()]));
-        return notifications == null
-                ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
+        StatusBarNotification[] notifications = getActiveNotifications(
+                keys.stream().map(n -> n.notificationKey).toArray(String[]::new));
+        return notifications == null ? Collections.emptyList() : Arrays.asList(notifications);
     }
 
     /**
-     * Filter out notifications that don't have an intent
-     * or are headers for grouped notifications.
-     *
-     * @see #shouldBeFilteredOut(StatusBarNotification)
+     * Returns true for notifications that have an intent and are not headers for grouped
+     * notifications and should be shown in the notification popup.
      */
-    private List<StatusBarNotification> filterNotifications(
-            StatusBarNotification[] notifications) {
-        if (notifications == null) return null;
-        IntSet removedNotifications = new IntSet();
-        for (int i = 0; i < notifications.length; i++) {
-            if (shouldBeFilteredOut(notifications[i])) {
-                removedNotifications.add(i);
-            }
-        }
-        List<StatusBarNotification> filteredNotifications = new ArrayList<>(
-                notifications.length - removedNotifications.size());
-        for (int i = 0; i < notifications.length; i++) {
-            if (!removedNotifications.contains(i)) {
-                filteredNotifications.add(notifications[i]);
-            }
-        }
-        return filteredNotifications;
-    }
-
-    private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
+    @WorkerThread
+    private boolean notificationIsValidForUI(StatusBarNotification sbn) {
         Notification notification = sbn.getNotification();
-
         updateGroupKeyIfNecessary(sbn);
 
         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
         if (!mTempRanking.canShowBadge()) {
-            return true;
+            return false;
         }
         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
             // Special filtering for the default, legacy "Miscellaneous" channel.
             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
-                return true;
+                return false;
             }
         }
 
@@ -382,19 +328,19 @@
         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
-        return (isGroupHeader || missingTitleAndText);
+        return !isGroupHeader && !missingTitleAndText;
+    }
+
+    private static Pair<PackageUserKey, NotificationKeyData> toKeyPair(StatusBarNotification sbn) {
+        return Pair.create(PackageUserKey.fromNotification(sbn),
+                NotificationKeyData.fromNotification(sbn));
     }
 
     public interface NotificationsChangedListener {
         void onNotificationPosted(PackageUserKey postedPackageUserKey,
-                NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
+                NotificationKeyData notificationKey);
         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
                 NotificationKeyData notificationKey);
         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
     }
-
-    public interface StatusBarNotificationsChangedListener {
-        void onNotificationPosted(StatusBarNotification sbn);
-        void onNotificationRemoved(StatusBarNotification sbn);
-    }
 }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b67adbb..b03aa9c 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -34,10 +34,10 @@
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -51,7 +51,7 @@
 @TargetApi(Build.VERSION_CODES.N)
 public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
 
-    private static FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
+    private static final FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
             new FloatProperty<NotificationMainView>("contentTranslation") {
         @Override
         public void setValue(NotificationMainView view, float v) {
@@ -176,7 +176,7 @@
 
     // SingleAxisSwipeDetector.Listener's
     @Override
-    public void onDragStart(boolean start) { }
+    public void onDragStart(boolean start, float startDisplacement) { }
 
 
     @Override
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 654e593..408796f 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -121,9 +121,9 @@
         mLinePaint.setAlpha(0);
 
         mLauncher = Launcher.getLauncher(context);
-        mLineHeight = res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_line_height);
+        mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height);
 
-        boolean darkText = WallpaperColorInfo.getInstance(context).supportsDarkText();
+        boolean darkText = WallpaperColorInfo.INSTANCE.get(context).supportsDarkText();
         mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
         mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
     }
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
new file mode 100644
index 0000000..901d27f
--- /dev/null
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2014 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.pm;
+
+import static com.android.launcher3.Utilities.getPrefs;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.SessionCommitReceiver;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Utility class to tracking install sessions
+ */
+public class InstallSessionHelper {
+
+    // Set<String> of session ids of promise icons that have been added to the home screen
+    // as FLAG_PROMISE_NEW_INSTALLS.
+    protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
+
+    private static final boolean DEBUG = false;
+
+    public static final MainThreadInitializedObject<InstallSessionHelper> INSTANCE =
+            new MainThreadInitializedObject<>(InstallSessionHelper::new);
+
+    private final LauncherApps mLauncherApps;
+    private final Context mAppContext;
+    private final IntSet mPromiseIconIds;
+
+    private final PackageInstaller mInstaller;
+    private final HashMap<String, Boolean> mSessionVerifiedMap = new HashMap<>();
+
+    public InstallSessionHelper(Context context) {
+        mInstaller = context.getPackageManager().getPackageInstaller();
+        mAppContext = context.getApplicationContext();
+        mLauncherApps = context.getSystemService(LauncherApps.class);
+
+        mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
+                getPrefs(context).getString(PROMISE_ICON_IDS, "")));
+
+        cleanUpPromiseIconIds();
+    }
+
+    public static UserHandle getUserHandle(SessionInfo info) {
+        return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle();
+    }
+
+    protected void cleanUpPromiseIconIds() {
+        IntArray existingIds = new IntArray();
+        for (SessionInfo info : getActiveSessions().values()) {
+            existingIds.add(info.getSessionId());
+        }
+        IntArray idsToRemove = new IntArray();
+
+        for (int i = mPromiseIconIds.size() - 1; i >= 0; --i) {
+            if (!existingIds.contains(mPromiseIconIds.getArray().get(i))) {
+                idsToRemove.add(mPromiseIconIds.getArray().get(i));
+            }
+        }
+        for (int i = idsToRemove.size() - 1; i >= 0; --i) {
+            mPromiseIconIds.getArray().removeValue(idsToRemove.get(i));
+        }
+    }
+
+    public HashMap<PackageUserKey, SessionInfo> getActiveSessions() {
+        HashMap<PackageUserKey, SessionInfo> activePackages = new HashMap<>();
+        for (SessionInfo info : getAllVerifiedSessions()) {
+            activePackages.put(new PackageUserKey(info.getAppPackageName(), getUserHandle(info)),
+                    info);
+        }
+        return activePackages;
+    }
+
+    public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) {
+        for (SessionInfo info : getAllVerifiedSessions()) {
+            boolean match = pkg.equals(info.getAppPackageName());
+            if (Utilities.ATLEAST_Q && !user.equals(getUserHandle(info))) {
+                match = false;
+            }
+            if (match) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    private void updatePromiseIconPrefs() {
+        getPrefs(mAppContext).edit()
+                .putString(PROMISE_ICON_IDS, mPromiseIconIds.getArray().toConcatString())
+                .apply();
+    }
+
+    SessionInfo getVerifiedSessionInfo(int sessionId) {
+        return verify(mInstaller.getSessionInfo(sessionId));
+    }
+
+    private SessionInfo verify(SessionInfo sessionInfo) {
+        if (sessionInfo == null
+                || sessionInfo.getInstallerPackageName() == null
+                || TextUtils.isEmpty(sessionInfo.getAppPackageName())) {
+            return null;
+        }
+        String pkg = sessionInfo.getInstallerPackageName();
+        synchronized (mSessionVerifiedMap) {
+            if (!mSessionVerifiedMap.containsKey(pkg)) {
+                boolean hasSystemFlag = new PackageManagerHelper(mAppContext).getApplicationInfo(
+                        pkg, getUserHandle(sessionInfo), ApplicationInfo.FLAG_SYSTEM) != null;
+                mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag);
+            }
+        }
+        return mSessionVerifiedMap.get(pkg) ? sessionInfo : null;
+    }
+
+    public List<SessionInfo> getAllVerifiedSessions() {
+        List<SessionInfo> list = new ArrayList<>(Utilities.ATLEAST_Q
+                ? mLauncherApps.getAllPackageInstallerSessions()
+                : mInstaller.getAllSessions());
+        Iterator<SessionInfo> it = list.iterator();
+        while (it.hasNext()) {
+            if (verify(it.next()) == null) {
+                it.remove();
+            }
+        }
+        return list;
+    }
+
+    /**
+     * Attempt to restore workspace layout if the session is triggered due to device restore.
+     */
+    public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
+        if (!Utilities.ATLEAST_OREO || !FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
+            return false;
+        }
+        if (isRestore(info)) {
+            LauncherSettings.Settings.call(mAppContext.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE);
+            return true;
+        }
+        return false;
+    }
+
+    @RequiresApi(26)
+    private static boolean isRestore(@NonNull final SessionInfo info) {
+        return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+    }
+
+    public boolean promiseIconAddedForId(int sessionId) {
+        return mPromiseIconIds.contains(sessionId);
+    }
+
+    public void removePromiseIconId(int sessionId) {
+        if (mPromiseIconIds.contains(sessionId)) {
+            mPromiseIconIds.getArray().removeValue(sessionId);
+            updatePromiseIconPrefs();
+        }
+    }
+
+    /**
+     * Add a promise app icon to the workspace iff:
+     * - The settings for it are enabled
+     * - The user installed the app
+     * - There is an app icon and label (For apps with no launching activity, no icon is provided).
+     * - The app is not already installed
+     * - A promise icon for the session has not already been created
+     */
+    void tryQueuePromiseAppIcon(PackageInstaller.SessionInfo sessionInfo) {
+        if (Utilities.ATLEAST_OREO && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
+                && SessionCommitReceiver.isEnabled(mAppContext)
+                && verify(sessionInfo) != null
+                && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
+                && sessionInfo.getAppIcon() != null
+                && !TextUtils.isEmpty(sessionInfo.getAppLabel())
+                && !mPromiseIconIds.contains(sessionInfo.getSessionId())
+                && new PackageManagerHelper(mAppContext).getApplicationInfo(
+                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo), 0) == null) {
+            SessionCommitReceiver.queuePromiseAppIconAddition(mAppContext, sessionInfo);
+            mPromiseIconIds.add(sessionInfo.getSessionId());
+            updatePromiseIconPrefs();
+        }
+    }
+
+    public InstallSessionTracker registerInstallTracker(
+            InstallSessionTracker.Callback callback, LooperExecutor executor) {
+        InstallSessionTracker tracker = new InstallSessionTracker(this, callback);
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            mInstaller.registerSessionCallback(tracker, executor.getHandler());
+        } else {
+            mLauncherApps.registerPackageInstallerSessionCallback(executor, tracker);
+        }
+        return tracker;
+    }
+
+    void unregister(InstallSessionTracker tracker) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            mInstaller.unregisterSessionCallback(tracker);
+        } else {
+            mLauncherApps.unregisterPackageInstallerSessionCallback(tracker);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
new file mode 100644
index 0000000..eb3ca73
--- /dev/null
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.pm;
+
+import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
+import static com.android.launcher3.pm.PackageInstallInfo.STATUS_FAILED;
+import static com.android.launcher3.pm.PackageInstallInfo.STATUS_INSTALLED;
+
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.launcher3.util.PackageUserKey;
+
+public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+
+    // Lazily initialized
+    private SparseArray<PackageUserKey> mActiveSessions = null;
+
+    private final InstallSessionHelper mInstallerCompat;
+    private final Callback mCallback;
+
+    InstallSessionTracker(InstallSessionHelper installerCompat, Callback callback) {
+        mInstallerCompat = installerCompat;
+        mCallback = callback;
+    }
+
+    @Override
+    public void onCreated(int sessionId) {
+        SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
+        if (sessionInfo != null) {
+            mCallback.onInstallSessionCreated(PackageInstallInfo.fromInstallingState(sessionInfo));
+        }
+
+        mInstallerCompat.tryQueuePromiseAppIcon(sessionInfo);
+    }
+
+    @Override
+    public void onFinished(int sessionId, boolean success) {
+        // For a finished session, we can't get the session info. So use the
+        // packageName from our local cache.
+        SparseArray<PackageUserKey> activeSessions = getActiveSessionMap();
+        PackageUserKey key = activeSessions.get(sessionId);
+        activeSessions.remove(sessionId);
+
+        if (key != null && key.mPackageName != null) {
+            String packageName = key.mPackageName;
+            PackageInstallInfo info = PackageInstallInfo.fromState(
+                    success ? STATUS_INSTALLED : STATUS_FAILED,
+                    packageName, key.mUser);
+            mCallback.onPackageStateChanged(info);
+
+            if (!success && mInstallerCompat.promiseIconAddedForId(sessionId)) {
+                mCallback.onSessionFailure(packageName, key.mUser);
+                // If it is successful, the id is removed in the the package added flow.
+                mInstallerCompat.removePromiseIconId(sessionId);
+            }
+        }
+    }
+
+    @Override
+    public void onProgressChanged(int sessionId, float progress) {
+        SessionInfo session = mInstallerCompat.getVerifiedSessionInfo(sessionId);
+        if (session != null && session.getAppPackageName() != null) {
+            mCallback.onPackageStateChanged(PackageInstallInfo.fromInstallingState(session));
+        }
+    }
+
+    @Override
+    public void onActiveChanged(int sessionId, boolean active) { }
+
+    @Override
+    public void onBadgingChanged(int sessionId) {
+        SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
+        if (sessionInfo != null) {
+            mInstallerCompat.tryQueuePromiseAppIcon(sessionInfo);
+        }
+    }
+
+    private SessionInfo pushSessionDisplayToLauncher(int sessionId) {
+        SessionInfo session = mInstallerCompat.getVerifiedSessionInfo(sessionId);
+        if (session != null && session.getAppPackageName() != null) {
+            PackageUserKey key =
+                    new PackageUserKey(session.getAppPackageName(), getUserHandle(session));
+            getActiveSessionMap().put(session.getSessionId(), key);
+            mCallback.onUpdateSessionDisplay(key, session);
+            return session;
+        }
+        return null;
+    }
+
+    private SparseArray<PackageUserKey> getActiveSessionMap() {
+        if (mActiveSessions == null) {
+            mActiveSessions = new SparseArray<>();
+            mInstallerCompat.getActiveSessions().forEach(
+                    (key, si) -> mActiveSessions.put(si.getSessionId(), key));
+        }
+        return mActiveSessions;
+    }
+
+    public void unregister() {
+        mInstallerCompat.unregister(this);
+    }
+
+    public interface Callback {
+
+        void onSessionFailure(String packageName, UserHandle user);
+
+        void onUpdateSessionDisplay(PackageUserKey key, SessionInfo info);
+
+        void onPackageStateChanged(PackageInstallInfo info);
+
+        void onInstallSessionCreated(PackageInstallInfo info);
+    }
+}
diff --git a/src/com/android/launcher3/pm/PackageInstallInfo.java b/src/com/android/launcher3/pm/PackageInstallInfo.java
new file mode 100644
index 0000000..7997d16
--- /dev/null
+++ b/src/com/android/launcher3/pm/PackageInstallInfo.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.pm;
+
+import android.content.ComponentName;
+import android.content.pm.PackageInstaller;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+public final class PackageInstallInfo {
+
+    public static final int STATUS_INSTALLED = 0;
+    public static final int STATUS_INSTALLING = 1;
+    public static final int STATUS_FAILED = 2;
+
+    public final ComponentName componentName;
+    public final String packageName;
+    public final int state;
+    public final int progress;
+    public final UserHandle user;
+
+    private PackageInstallInfo(@NonNull PackageInstaller.SessionInfo info) {
+        this.state = STATUS_INSTALLING;
+        this.packageName = info.getAppPackageName();
+        this.componentName = new ComponentName(packageName, "");
+        this.progress = (int) (info.getProgress() * 100f);
+        this.user = InstallSessionHelper.getUserHandle(info);
+    }
+
+    public PackageInstallInfo(String packageName, int state, int progress, UserHandle user) {
+        this.state = state;
+        this.packageName = packageName;
+        this.componentName = new ComponentName(packageName, "");
+        this.progress = progress;
+        this.user = user;
+    }
+
+    public static PackageInstallInfo fromInstallingState(PackageInstaller.SessionInfo info) {
+        return new PackageInstallInfo(info);
+    }
+
+    public static PackageInstallInfo fromState(int state, String packageName, UserHandle user) {
+        return new PackageInstallInfo(packageName, state, 0 /* progress */, user);
+    }
+
+}
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
new file mode 100644
index 0000000..179061f
--- /dev/null
+++ b/src/com/android/launcher3/pm/PinRequestHelper.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 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.pm;
+
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.PinItemRequest;
+import android.content.pm.ShortcutInfo;
+import android.os.Build;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.ShortcutCachingLogic;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+public class PinRequestHelper {
+
+    /**
+     * request.accept() will initiate the following flow:
+     *      -> go-to-system-process for actual processing (a)
+     *      -> callback-to-launcher on UI thread (b)
+     *      -> post callback on the worker thread (c)
+     *      -> Update model and unpin (in system) any shortcut not in out model. (d)
+     *
+     * Note that (b) will take at-least one frame as it involves posting callback from binder
+     * thread to UI thread.
+     * If (d) happens before we add this shortcut to our model, we will end up unpinning
+     * the shortcut in the system.
+     * Here its the caller's responsibility to add the newly created WorkspaceItemInfo immediately
+     * to the model (which may involves a single post-to-worker-thread). That will guarantee
+     * that (d) happens after model is updated.
+     */
+    @Nullable
+    @TargetApi(Build.VERSION_CODES.O)
+    public static WorkspaceItemInfo createWorkspaceItemFromPinItemRequest(
+            Context context, final PinItemRequest request, final long acceptDelay) {
+        if (request != null && request.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT
+                && request.isValid()) {
+
+            if (acceptDelay <= 0) {
+                if (!request.accept()) {
+                    return null;
+                }
+            } else {
+                // Block the worker thread until the accept() is called.
+                MODEL_EXECUTOR.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            Thread.sleep(acceptDelay);
+                        } catch (InterruptedException e) {
+                            // Ignore
+                        }
+                        if (request.isValid()) {
+                            request.accept();
+                        }
+                    }
+                });
+            }
+
+            ShortcutInfo si = request.getShortcutInfo();
+            WorkspaceItemInfo info = new WorkspaceItemInfo(si, context);
+            // Apply the unbadged icon synchronously using the caching logic directly and
+            // fetch the actual icon asynchronously.
+            info.bitmap = new ShortcutCachingLogic().loadIcon(context, si);
+            LauncherAppState.getInstance(context).getModel().updateAndBindWorkspaceItem(info, si);
+            return info;
+        } else {
+            return null;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.O)
+    public static PinItemRequest getPinItemRequest(Intent intent) {
+        Parcelable extra = intent.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST);
+        return extra instanceof PinItemRequest ? (PinItemRequest) extra : null;
+    }
+}
diff --git a/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
similarity index 65%
rename from src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java
rename to src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index ace5691..40d7031 100644
--- a/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -14,34 +14,44 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.compat;
+package com.android.launcher3.pm;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
 import android.widget.Toast;
 
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.ComponentWithLabel;
-import com.android.launcher3.icons.IconCache;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Wrapper class for representing a shortcut configure activity.
  */
-public abstract class ShortcutConfigActivityInfo implements ComponentWithLabel {
+public abstract class ShortcutConfigActivityInfo implements ComponentWithLabelAndIcon {
 
     private static final String TAG = "SCActivityInfo";
 
@@ -67,6 +77,7 @@
         return LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
     }
 
+    @Override
     public abstract Drawable getFullResIcon(IconCache cache);
 
     /**
@@ -87,9 +98,9 @@
             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
         } catch (SecurityException e) {
             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
-            Log.e(TAG, "Launcher does not have the permission to launch " + intent +
-                    ". Make sure to create a MAIN intent-filter for the corresponding activity " +
-                    "or use the exported attribute for this activity.", e);
+            Log.e(TAG, "Launcher does not have the permission to launch " + intent
+                    + ". Make sure to create a MAIN intent-filter for the corresponding activity "
+                    + "or use the exported attribute for this activity.", e);
         }
         return false;
     }
@@ -106,7 +117,7 @@
 
         private final ActivityInfo mInfo;
 
-        public ShortcutConfigActivityInfoVL(ActivityInfo info) {
+        ShortcutConfigActivityInfoVL(ActivityInfo info) {
             super(new ComponentName(info.packageName, info.name), Process.myUserHandle());
             mInfo = info;
         }
@@ -158,4 +169,46 @@
             }
         }
     }
+
+    public static List<ShortcutConfigActivityInfo> queryList(
+            Context context, @Nullable PackageUserKey packageUser) {
+        List<ShortcutConfigActivityInfo> result = new ArrayList<>();
+        UserHandle myUser = Process.myUserHandle();
+
+        if (Utilities.ATLEAST_OREO) {
+            final List<UserHandle> users;
+            final String packageName;
+            if (packageUser == null) {
+                users = UserCache.INSTANCE.get(context).getUserProfiles();
+                packageName = null;
+            } else {
+                users = new ArrayList<>(1);
+                users.add(packageUser.mUser);
+                packageName = packageUser.mPackageName;
+            }
+            LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+            for (UserHandle user : users) {
+                boolean ignoreTargetSdk = myUser.equals(user);
+                for (LauncherActivityInfo activityInfo :
+                        launcherApps.getShortcutConfigActivityList(packageName, user)) {
+                    if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion
+                            >= Build.VERSION_CODES.O) {
+                        result.add(new ShortcutConfigActivityInfoVO(activityInfo));
+                    }
+                }
+            }
+        } else {
+            if (packageUser == null || packageUser.mUser.equals(myUser)) {
+                Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+                if (packageUser != null) {
+                    intent.setPackage(packageUser.mPackageName);
+                }
+                for (ResolveInfo info :
+                        context.getPackageManager().queryIntentActivities(intent, 0)) {
+                    result.add(new ShortcutConfigActivityInfoVL(info.activityInfo));
+                }
+            }
+        }
+        return result;
+    }
 }
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
new file mode 100644
index 0000000..5aab41a
--- /dev/null
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2014 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.pm;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.LongSparseArray;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class which manages a local cache of user handles to avoid system rpc
+ */
+public class UserCache {
+
+    public static final MainThreadInitializedObject<UserCache> INSTANCE =
+            new MainThreadInitializedObject<>(UserCache::new);
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+    private final ArrayList<Runnable> mUserChangeListeners = new ArrayList<>();
+    private final SimpleBroadcastReceiver mUserChangeReceiver =
+            new SimpleBroadcastReceiver(this::onUsersChanged);
+
+    private LongSparseArray<UserHandle> mUsers;
+    // Create a separate reverse map as LongSparseArray.indexOfValue checks if objects are same
+    // and not {@link Object#equals}
+    private ArrayMap<UserHandle, Long> mUserToSerialMap;
+
+    private UserCache(Context context) {
+        mContext = context;
+        mUserManager = context.getSystemService(UserManager.class);
+    }
+
+    private void onUsersChanged(Intent intent) {
+        enableAndResetCache();
+        mUserChangeListeners.forEach(Runnable::run);
+    }
+
+    /**
+     * Adds a listener for user additions and removals
+     */
+    public SafeCloseable addUserChangeListener(Runnable command) {
+        synchronized (this) {
+            if (mUserChangeListeners.isEmpty()) {
+                // Enable caching and start listening for user broadcast
+                mUserChangeReceiver.register(mContext,
+                        Intent.ACTION_MANAGED_PROFILE_ADDED,
+                        Intent.ACTION_MANAGED_PROFILE_REMOVED);
+                enableAndResetCache();
+            }
+            mUserChangeListeners.add(command);
+            return () -> removeUserChangeListener(command);
+        }
+    }
+
+    private void enableAndResetCache() {
+        synchronized (this) {
+            mUsers = new LongSparseArray<>();
+            mUserToSerialMap = new ArrayMap<>();
+            List<UserHandle> users = mUserManager.getUserProfiles();
+            if (users != null) {
+                for (UserHandle user : users) {
+                    long serial = mUserManager.getSerialNumberForUser(user);
+                    mUsers.put(serial, user);
+                    mUserToSerialMap.put(user, serial);
+                }
+            }
+        }
+    }
+
+    private void removeUserChangeListener(Runnable command) {
+        synchronized (this) {
+            mUserChangeListeners.remove(command);
+            if (mUserChangeListeners.isEmpty()) {
+                // Disable cache and stop listening
+                mContext.unregisterReceiver(mUserChangeReceiver);
+
+                mUsers = null;
+                mUserToSerialMap = null;
+            }
+        }
+    }
+
+    /**
+     * @see UserManager#getSerialNumberForUser(UserHandle)
+     */
+    public long getSerialNumberForUser(UserHandle user) {
+        synchronized (this) {
+            if (mUserToSerialMap != null) {
+                Long serial = mUserToSerialMap.get(user);
+                return serial == null ? 0 : serial;
+            }
+        }
+        return mUserManager.getSerialNumberForUser(user);
+    }
+
+    /**
+     * @see UserManager#getUserForSerialNumber(long)
+     */
+    public UserHandle getUserForSerialNumber(long serialNumber) {
+        synchronized (this) {
+            if (mUsers != null) {
+                return mUsers.get(serialNumber);
+            }
+        }
+        return mUserManager.getUserForSerialNumber(serialNumber);
+    }
+
+    /**
+     * @see UserManager#getUserProfiles()
+     */
+    public List<UserHandle> getUserProfiles() {
+        synchronized (this) {
+            if (mUsers != null) {
+                return new ArrayList<>(mUserToSerialMap.keySet());
+            }
+        }
+
+        List<UserHandle> users = mUserManager.getUserProfiles();
+        return users == null ? Collections.emptyList() : users;
+    }
+}
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 98f7fd8..d5b32fc 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -32,6 +32,7 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
 import android.util.AttributeSet;
+import android.util.Pair;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -40,8 +41,8 @@
 import android.widget.FrameLayout;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -57,14 +58,16 @@
 
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
+ *
+ * @param <T> The activity on with the popup shows
  */
-public abstract class ArrowPopup extends AbstractFloatingView {
+public abstract class ArrowPopup<T extends BaseDraggingActivity> extends AbstractFloatingView {
 
     private final Rect mTempRect = new Rect();
 
     protected final LayoutInflater mInflater;
     private final float mOutlineRadius;
-    protected final Launcher mLauncher;
+    protected final T mLauncher;
     protected final boolean mIsRtl;
 
     private final int mArrowOffset;
@@ -83,7 +86,7 @@
         super(context, attrs, defStyleAttr);
         mInflater = LayoutInflater.from(context);
         mOutlineRadius = Themes.getDialogCornerRadius(context);
-        mLauncher = Launcher.getLauncher(context);
+        mLauncher = BaseDraggingActivity.fromContext(context);
         mIsRtl = Utilities.isRtl(getResources());
 
         setClipToOutline(true);
@@ -120,16 +123,22 @@
         }
     }
 
-    public <T extends View> T inflateAndAdd(int resId, ViewGroup container) {
+    /**
+     * Utility method for inflating and adding a view
+     */
+    public <R extends View> R inflateAndAdd(int resId, ViewGroup container) {
         View view = mInflater.inflate(resId, container, false);
         container.addView(view);
-        return (T) view;
+        return (R) view;
     }
 
-    public <T extends View> T inflateAndAdd(int resId, ViewGroup container, int index) {
+    /**
+     * Utility method for inflating and adding a view
+     */
+    public <R extends View> R inflateAndAdd(int resId, ViewGroup container, int index) {
         View view = mInflater.inflate(resId, container, false);
         container.addView(view, index);
-        return (T) view;
+        return (R) view;
     }
 
     /**
@@ -231,6 +240,17 @@
      * and align above if there is enough vertical space.
      */
     protected void orientAboutObject() {
+        orientAboutObject(true /* allowAlignLeft */, true /* allowAlignRight */);
+    }
+
+    /**
+     * @see #orientAboutObject()
+     *
+     * @param allowAlignLeft Set to false if we already tried aligning left and didn't have room.
+     * @param allowAlignRight Set to false if we already tried aligning right and didn't have room.
+     * TODO: Can we test this with all permutations of widths/heights and icon locations + RTL?
+     */
+    private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
         int width = getMeasuredWidth();
         int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset
@@ -244,14 +264,8 @@
         // Align left (right in RTL) if there is room.
         int leftAlignedX = mTempRect.left;
         int rightAlignedX = mTempRect.right - width;
-        int x = leftAlignedX;
-        boolean canBeLeftAligned = leftAlignedX + width + insets.left
-                < dragLayer.getRight() - insets.right;
-        boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
-        if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
-            x = rightAlignedX;
-        }
-        mIsLeftAligned = x == leftAlignedX;
+        mIsLeftAligned = !mIsRtl ? allowAlignLeft : !allowAlignRight;
+        int x = mIsLeftAligned ? leftAlignedX : rightAlignedX;
 
         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
         int iconWidth = mTempRect.width();
@@ -273,6 +287,24 @@
         }
         x += mIsLeftAligned ? xOffset : -xOffset;
 
+        // Check whether we can still align as we originally wanted, now that we've calculated x.
+        if (!allowAlignLeft && !allowAlignRight) {
+            // We've already tried both ways and couldn't make it fit. onLayout() will set the
+            // gravity to CENTER_HORIZONTAL, but continue below to update y.
+        } else {
+            boolean canBeLeftAligned = x + width + insets.left
+                    < dragLayer.getWidth() - insets.right;
+            boolean canBeRightAligned = x > insets.left;
+            boolean alignmentStillValid = mIsLeftAligned && canBeLeftAligned
+                    || !mIsLeftAligned && canBeRightAligned;
+            if (!alignmentStillValid) {
+                // Try again, but don't allow this alignment we already know won't work.
+                orientAboutObject(allowAlignLeft && !mIsLeftAligned /* allowAlignLeft */,
+                        allowAlignRight && mIsLeftAligned /* allowAlignRight */);
+                return;
+            }
+        }
+
         // Open above icon if there is room.
         int iconHeight = mTempRect.height();
         int y = mTempRect.top - height;
@@ -335,8 +367,10 @@
         super.onLayout(changed, l, t, r, b);
 
         // enforce contained is within screen
-        ViewGroup dragLayer = getPopupContainer();
-        if (getTranslationX() + l < 0 || getTranslationX() + r > dragLayer.getWidth()) {
+        BaseDragLayer dragLayer = getPopupContainer();
+        Rect insets = dragLayer.getInsets();
+        if (getTranslationX() + l < insets.left
+                || getTranslationX() + r > dragLayer.getWidth() - insets.right) {
             // If we are still off screen, center horizontally too.
             mGravity |= Gravity.CENTER_HORIZONTAL;
         }
@@ -350,6 +384,16 @@
         }
     }
 
+    @Override
+    protected Pair<View, String> getAccessibilityTarget() {
+        return Pair.create(this, "");
+    }
+
+    @Override
+    protected View getAccessibilityInitialFocusView() {
+        return getChildCount() > 0 ? getChildAt(0) : this;
+    }
+
     private void animateOpen() {
         setVisibility(View.VISIBLE);
 
@@ -405,6 +449,8 @@
         }
         if (getOutlineProvider() instanceof RevealOutlineAnimation) {
             ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
+        } else {
+            mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
         }
         if (mOpenCloseAnimator != null) {
             mOpenCloseAnimator.cancel();
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index e8ac1d4..614cf14 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -16,8 +16,10 @@
 
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO;
 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
@@ -37,37 +39,36 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
@@ -76,21 +77,22 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
+ *
+ * @param <T> The activity on with the popup shows
  */
-public class PopupContainerWithArrow extends ArrowPopup implements DragSource,
-        DragController.DragListener, View.OnLongClickListener,
-        View.OnTouchListener, PopupDataChangeListener {
+public class PopupContainerWithArrow<T extends BaseDraggingActivity> extends ArrowPopup<T>
+        implements DragSource, DragController.DragListener {
 
     private final List<DeepShortcutView> mShortcuts = new ArrayList<>();
     private final PointF mInterceptTouchDown = new PointF();
-    protected final Point mIconLastTouchPos = new Point();
 
     private final int mStartDragThreshold;
-    private final LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     private BubbleTextView mOriginalIcon;
     private NotificationItemView mNotificationItemView;
@@ -98,11 +100,13 @@
 
     private ViewGroup mSystemShortcutContainer;
 
+    protected PopupItemDragHandler mPopupItemDragHandler;
+    protected LauncherAccessibilityDelegate mAccessibilityDelegate;
+
     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mStartDragThreshold = getResources().getDimensionPixelSize(
                 R.dimen.deep_shortcuts_start_drag_threshold);
-        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
     }
 
     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
@@ -118,18 +122,6 @@
     }
 
     @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mLauncher.getPopupDataProvider().setChangeListener(this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mLauncher.getPopupDataProvider().setChangeListener(null);
-    }
-
-    @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mInterceptTouchDown.set(ev.getX(), ev.getY());
@@ -169,18 +161,22 @@
 
     public OnClickListener getItemClickListener() {
         return (view) -> {
-            ItemClickHandler.INSTANCE.onClick(view);
+            mLauncher.getItemOnClickListener().onClick(view);
             close(true);
         };
     }
 
+    public PopupItemDragHandler getItemDragHandler() {
+        return mPopupItemDragHandler;
+    }
+
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             BaseDragLayer dl = getPopupContainer();
             if (!dl.isEventOverView(this, ev)) {
                 mLauncher.getUserEventDispatcher().logActionTapOutside(
-                        LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS));
+                        newContainerTarget(ContainerType.DEEPSHORTCUTS));
                 close(true);
 
                 // We let touches on the original icon go through so that users can launch
@@ -192,31 +188,52 @@
     }
 
     /**
+     * Returns true if we can show the container.
+     */
+    public static boolean canShow(View icon, ItemInfo item) {
+        return icon instanceof BubbleTextView && ShortcutUtil.supportsShortcuts(item);
+    }
+
+    /**
      * Shows the notifications and deep shortcuts associated with {@param icon}.
      * @return the container if shown or null.
      */
     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "showForIcon");
-        }
         Launcher launcher = Launcher.getLauncher(icon.getContext());
         if (getOpen(launcher) != null) {
             // There is already an items container open, so don't open this one.
             icon.clearFocus();
             return null;
         }
-        ItemInfo itemInfo = (ItemInfo) icon.getTag();
-        if (!ShortcutUtil.supportsShortcuts(itemInfo)) {
+        ItemInfo item = (ItemInfo) icon.getTag();
+        if (!canShow(icon, item)) {
             return null;
         }
 
         final PopupContainerWithArrow container =
                 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                         R.layout.popup_container, launcher.getDragLayer(), false);
-        container.populateAndShow(icon, itemInfo, SystemShortcutFactory.INSTANCE.get(launcher));
+        container.configureForLauncher(launcher);
+
+        PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
+        container.populateAndShow(icon,
+                popupDataProvider.getShortcutCountForItem(item),
+                popupDataProvider.getNotificationKeysForItem(item),
+                launcher.getSupportedShortcuts()
+                        .map(s -> s.getShortcut(launcher, item))
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList()));
+        launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
         return container;
     }
 
+    private void configureForLauncher(Launcher launcher) {
+        addOnAttachStateChangeListener(new LiveUpdateHandler(launcher));
+        mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
+        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
+        launcher.getDragController().addDragListener(this);
+    }
+
     @Override
     protected void onInflationComplete(boolean isReversed) {
         if (isReversed && mNotificationItemView != null) {
@@ -238,28 +255,22 @@
         }
     }
 
-    protected void populateAndShow(
-            BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "populateAndShow");
-        }
-        PopupDataProvider popupDataProvider = mLauncher.getPopupDataProvider();
-        populateAndShow(icon,
-                popupDataProvider.getShortcutCountForItem(item),
-                popupDataProvider.getNotificationKeysForItem(item),
-                factory.getEnabledShortcuts(mLauncher, item));
-    }
-
-    public ViewGroup getSystemShortcutContainerForTesting() {
-        return mSystemShortcutContainer;
-    }
-
     @TargetApi(Build.VERSION_CODES.P)
-    protected void populateAndShow(final BubbleTextView originalIcon, int shortcutCount,
+    public void populateAndShow(final BubbleTextView originalIcon, int shortcutCount,
             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
         mNumNotifications = notificationKeys.size();
         mOriginalIcon = originalIcon;
 
+        boolean hasDeepShortcuts = shortcutCount > 0;
+        int containerWidth = (int) getResources().getDimension(R.dimen.bg_popup_item_width);
+
+        // if there are deep shortcuts, we might want to increase the width of shortcuts to fit
+        // horizontally laid out system shortcuts.
+        if (hasDeepShortcuts) {
+            containerWidth = (int) Math.max(containerWidth,
+                    systemShortcuts.size() * getResources().getDimension(
+                            R.dimen.system_shortcut_header_icon_touch_size));
+        }
         // Add views
         if (mNumNotifications > 0) {
             // Add notification entries
@@ -268,18 +279,22 @@
             if (mNumNotifications == 1) {
                 mNotificationItemView.removeFooter();
             }
+            else {
+                mNotificationItemView.setFooterWidth(containerWidth);
+            }
             updateNotificationHeader();
         }
         int viewsToFlip = getChildCount();
         mSystemShortcutContainer = this;
-
-        if (shortcutCount > 0) {
+        if (hasDeepShortcuts) {
             if (mNotificationItemView != null) {
                 mNotificationItemView.addGutter();
             }
 
             for (int i = shortcutCount; i > 0; i--) {
-                mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this));
+                DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, this);
+                v.getLayoutParams().width = containerWidth;
+                mShortcuts.add(v);
             }
             updateHiddenShortcuts();
 
@@ -307,7 +322,6 @@
             setAccessibilityPaneTitle(getTitleForAccessibility());
         }
 
-        mLauncher.getDragController().addDragListener(this);
         mOriginalIcon.setForceHideDot(true);
 
         // All views are added. Animate layout from now on.
@@ -326,11 +340,6 @@
     }
 
     @Override
-    protected Pair<View, String> getAccessibilityTarget() {
-        return Pair.create(this, "");
-    }
-
-    @Override
     protected void getTargetObjectLocation(Rect outPos) {
         getPopupContainer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos);
         outPos.top += mOriginalIcon.getPaddingTop();
@@ -342,7 +351,9 @@
     }
 
     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
-        mNotificationItemView.applyNotificationInfos(notificationInfos);
+        if (mNotificationItemView != null) {
+            mNotificationItemView.applyNotificationInfos(notificationInfos);
+        }
     }
 
     private void updateHiddenShortcuts() {
@@ -379,45 +390,6 @@
         }
     }
 
-    @Override
-    public void onWidgetsBound() {
-        ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-        SystemShortcut widgetInfo = new SystemShortcut.Widgets();
-        View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo);
-        View widgetsView = null;
-        int count = mSystemShortcutContainer.getChildCount();
-        for (int i = 0; i < count; i++) {
-            View systemShortcutView = mSystemShortcutContainer.getChildAt(i);
-            if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
-                widgetsView = systemShortcutView;
-                break;
-            }
-        }
-
-        if (onClickListener != null && widgetsView == null) {
-            // We didn't have any widgets cached but now there are some, so enable the shortcut.
-            if (mSystemShortcutContainer != this) {
-                initializeSystemShortcut(
-                        R.layout.system_shortcut_icon_only, mSystemShortcutContainer, widgetInfo);
-            } else {
-                // If using the expanded system shortcut (as opposed to just the icon), we need to
-                // reopen the container to ensure measurements etc. all work out. While this could
-                // be quite janky, in practice the user would typically see a small flicker as the
-                // animation restarts partway through, and this is a very rare edge case anyway.
-                close(false);
-                PopupContainerWithArrow.showForIcon(mOriginalIcon);
-            }
-        } else if (onClickListener == null && widgetsView != null) {
-            // No widgets exist, but we previously added the shortcut so remove it.
-            if (mSystemShortcutContainer != this) {
-                mSystemShortcutContainer.removeView(widgetsView);
-            } else {
-                close(false);
-                PopupContainerWithArrow.showForIcon(mOriginalIcon);
-            }
-        }
-    }
-
     private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
         View view = inflateAndAdd(
                 resId, container, getInsertIndexForSystemShortcut(container, info));
@@ -428,10 +400,12 @@
         } else if (view instanceof ImageView) {
             // Only the system shortcut icon shows on a gray background header.
             info.setIconAndContentDescriptionFor((ImageView) view);
+            if (Utilities.ATLEAST_OREO) {
+                view.setTooltipText(view.getContentDescription());
+            }
         }
         view.setTag(info);
-        view.setOnClickListener(info.getOnClickListener(mLauncher,
-                (ItemInfo) mOriginalIcon.getTag()));
+        view.setOnClickListener(info);
     }
 
     /**
@@ -489,43 +463,12 @@
         };
     }
 
-    /**
-     * Updates the notification header if the original icon's dot updated.
-     */
-    @Override
-    public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) {
-        ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-        PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
-        if (updatedDots.test(packageUser)) {
-            updateNotificationHeader();
-        }
-    }
-
     private void updateNotificationHeader() {
         ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
         DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
         if (mNotificationItemView != null && dotInfo != null) {
             mNotificationItemView.updateHeader(
-                    dotInfo.getNotificationCount(), itemInfo.iconColor);
-        }
-    }
-
-    @Override
-    public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
-        if (mNotificationItemView == null) {
-            return;
-        }
-        ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
-        DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
-        if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
-            // No more notifications, remove the notification views and expand all shortcuts.
-            mNotificationItemView.removeAllViews();
-            mNotificationItemView = null;
-            updateHiddenShortcuts();
-            updateDividers();
-        } else {
-            mNotificationItemView.trimNotifications(
-                    NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
+                    dotInfo.getNotificationCount(), itemInfo.bitmap.color);
         }
     }
 
@@ -556,14 +499,15 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        if (info == NOTIFICATION_ITEM_INFO) {
-            target.itemType = ItemType.NOTIFICATION;
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        if (childInfo == NOTIFICATION_ITEM_INFO) {
+            child.itemType = ItemType.NOTIFICATION;
         } else {
-            target.itemType = ItemType.DEEPSHORTCUT;
-            target.rank = info.rank;
+            child.itemType = ItemType.DEEPSHORTCUT;
+            child.rank = childInfo.rank;
         }
-        targetParent.containerType = ContainerType.DEEPSHORTCUTS;
+        parents.add(newContainerTarget(ContainerType.DEEPSHORTCUTS));
     }
 
     @Override
@@ -583,47 +527,167 @@
         super.closeComplete();
     }
 
-    @Override
-    public boolean onTouch(View v, MotionEvent ev) {
-        // Touched a shortcut, update where it was touched so we can drag from there on long click.
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
-                break;
-        }
-        return false;
-    }
-
-    @Override
-    public boolean onLongClick(View v) {
-        if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
-        // Return early if not the correct view
-        if (!(v.getParent() instanceof DeepShortcutView)) return false;
-
-        // Long clicked on a shortcut.
-        DeepShortcutView sv = (DeepShortcutView) v.getParent();
-        sv.setWillDrawIcon(false);
-
-        // Move the icon to align with the center-top of the touch point
-        Point iconShift = new Point();
-        iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
-        iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
-
-        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(),
-                this, sv.getFinalInfo(),
-                new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions());
-        dv.animateShift(-iconShift.x, -iconShift.y);
-
-        // TODO: support dragging from within folder without having to close it
-        AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
-        return false;
-    }
-
     /**
      * Returns a PopupContainerWithArrow which is already open or null
      */
-    public static PopupContainerWithArrow getOpen(Launcher launcher) {
+    public static PopupContainerWithArrow getOpen(BaseDraggingActivity launcher) {
         return getOpenView(launcher, TYPE_ACTION_POPUP);
     }
+
+    /**
+     * Utility class to handle updates while the popup is visible (like widgets and
+     * notification changes)
+     */
+    private class LiveUpdateHandler implements
+            PopupDataChangeListener, View.OnAttachStateChangeListener {
+
+        private final Launcher mLauncher;
+
+        LiveUpdateHandler(Launcher launcher) {
+            mLauncher = launcher;
+        }
+
+        @Override
+        public void onViewAttachedToWindow(View view) {
+            mLauncher.getPopupDataProvider().setChangeListener(this);
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View view) {
+            mLauncher.getPopupDataProvider().setChangeListener(null);
+        }
+
+        @Override
+        public void onWidgetsBound() {
+            ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
+            SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
+            View widgetsView = null;
+            int count = mSystemShortcutContainer.getChildCount();
+            for (int i = 0; i < count; i++) {
+                View systemShortcutView = mSystemShortcutContainer.getChildAt(i);
+                if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
+                    widgetsView = systemShortcutView;
+                    break;
+                }
+            }
+
+            if (widgetInfo != null && widgetsView == null) {
+                // We didn't have any widgets cached but now there are some, so enable the shortcut.
+                if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
+                    initializeSystemShortcut(R.layout.system_shortcut_icon_only,
+                            mSystemShortcutContainer, widgetInfo);
+                } else {
+                    // If using the expanded system shortcut (as opposed to just the icon), we need
+                    // to reopen the container to ensure measurements etc. all work out. While this
+                    // could be quite janky, in practice the user would typically see a small
+                    // flicker as the animation restarts partway through, and this is a very rare
+                    // edge case anyway.
+                    close(false);
+                    PopupContainerWithArrow.showForIcon(mOriginalIcon);
+                }
+            } else if (widgetInfo == null && widgetsView != null) {
+                // No widgets exist, but we previously added the shortcut so remove it.
+                if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
+                    mSystemShortcutContainer.removeView(widgetsView);
+                } else {
+                    close(false);
+                    PopupContainerWithArrow.showForIcon(mOriginalIcon);
+                }
+            }
+        }
+
+        /**
+         * Updates the notification header if the original icon's dot updated.
+         */
+        @Override
+        public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) {
+            ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
+            PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
+            if (updatedDots.test(packageUser)) {
+                updateNotificationHeader();
+            }
+        }
+
+
+        @Override
+        public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
+            if (mNotificationItemView == null) {
+                return;
+            }
+            ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
+            DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
+            if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
+                // No more notifications, remove the notification views and expand all shortcuts.
+                mNotificationItemView.removeAllViews();
+                mNotificationItemView = null;
+                updateHiddenShortcuts();
+                updateDividers();
+            } else {
+                mNotificationItemView.trimNotifications(
+                        NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
+            }
+        }
+    }
+
+    /**
+     * Handler to control drag-and-drop for popup items
+     */
+    public interface PopupItemDragHandler extends OnLongClickListener, OnTouchListener { }
+
+    /**
+     * Drag and drop handler for popup items in Launcher activity
+     */
+    public static class LauncherPopupItemDragHandler implements PopupItemDragHandler {
+
+        protected final Point mIconLastTouchPos = new Point();
+        private final Launcher mLauncher;
+        private final PopupContainerWithArrow mContainer;
+
+        LauncherPopupItemDragHandler(Launcher launcher, PopupContainerWithArrow container) {
+            mLauncher = launcher;
+            mContainer = container;
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent ev) {
+            // Touched a shortcut, update where it was touched so we can drag from there on
+            // long click.
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_MOVE:
+                    mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+                    break;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onLongClick(View v) {
+            if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+            // Return early if not the correct view
+            if (!(v.getParent() instanceof DeepShortcutView)) return false;
+
+            // Long clicked on a shortcut.
+            DeepShortcutView sv = (DeepShortcutView) v.getParent();
+            sv.setWillDrawIcon(false);
+
+            // Move the icon to align with the center-top of the touch point
+            Point iconShift = new Point();
+            iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
+            iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
+
+            DraggableView draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
+            WorkspaceItemInfo itemInfo = sv.getFinalInfo();
+            itemInfo.container = CONTAINER_SHORTCUTS;
+            DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), draggableView,
+                    mContainer, itemInfo,
+                    new ShortcutDragPreviewProvider(sv.getIconView(), iconShift),
+                    new DragOptions());
+            dv.animateShift(-iconShift.x, -iconShift.y);
+
+            // TODO: support dragging from within folder without having to close it
+            AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 4612b2a..5a5f668 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -20,18 +20,20 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 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 java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -39,13 +41,10 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * Provides data for the popup menu that appears after long-clicking on apps.
  */
@@ -54,7 +53,7 @@
     private static final boolean LOGD = false;
     private static final String TAG = "PopupDataProvider";
 
-    private final Launcher mLauncher;
+    private final Consumer<Predicate<PackageUserKey>> mNotificationDotsChangeListener;
 
     /** Maps launcher activity components to a count of how many shortcuts they have. */
     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
@@ -65,39 +64,25 @@
 
     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
 
-    public PopupDataProvider(Launcher launcher) {
-        mLauncher = launcher;
+    public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
+        mNotificationDotsChangeListener = notificationDotsChangeListener;
     }
 
     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        mLauncher.updateNotificationDots(updatedDots);
+        mNotificationDotsChangeListener.accept(updatedDots);
         mChangeListener.onNotificationDotsUpdated(updatedDots);
     }
 
     @Override
     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
-            NotificationKeyData notificationKey, boolean shouldBeFilteredOut) {
+            NotificationKeyData notificationKey) {
         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
-        boolean dotShouldBeRefreshed;
         if (dotInfo == null) {
-            if (!shouldBeFilteredOut) {
-                DotInfo newDotInfo = new DotInfo();
-                newDotInfo.addOrUpdateNotificationKey(notificationKey);
-                mPackageUserToDotInfos.put(postedPackageUserKey, newDotInfo);
-                dotShouldBeRefreshed = true;
-            } else {
-                dotShouldBeRefreshed = false;
-            }
-        } else {
-            dotShouldBeRefreshed = shouldBeFilteredOut
-                    ? dotInfo.removeNotificationKey(notificationKey)
-                    : dotInfo.addOrUpdateNotificationKey(notificationKey);
-            if (dotInfo.getNotificationKeys().size() == 0) {
-                mPackageUserToDotInfos.remove(postedPackageUserKey);
-            }
+            dotInfo = new DotInfo();
+            mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
         }
-        if (dotShouldBeRefreshed) {
-            updateNotificationDots(t -> postedPackageUserKey.equals(t));
+        if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
+            updateNotificationDots(postedPackageUserKey::equals);
         }
     }
 
@@ -109,7 +94,7 @@
             if (oldDotInfo.getNotificationKeys().size() == 0) {
                 mPackageUserToDotInfos.remove(removedPackageUserKey);
             }
-            updateNotificationDots(t -> removedPackageUserKey.equals(t));
+            updateNotificationDots(removedPackageUserKey::equals);
             trimNotifications(mPackageUserToDotInfos);
         }
     }
@@ -195,14 +180,6 @@
                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
     }
 
-    /** This makes a potentially expensive binder call and should be run on a background thread. */
-    public @NonNull List<StatusBarNotification> getStatusBarNotificationsForKeys(
-            List<NotificationKeyData> notificationKeys) {
-        NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
-        return notificationListener == null ? Collections.EMPTY_LIST
-                : notificationListener.getNotificationsForKeys(notificationKeys);
-    }
-
     public void cancelNotification(String notificationKey) {
         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
         if (notificationListener == null) {
@@ -262,6 +239,11 @@
                 }).collect(Collectors.toList());
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "PopupDataProvider:");
+        writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
+    }
+
     public interface PopupDataChangeListener {
 
         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index dbfe988..5ed6f2e 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -16,30 +16,33 @@
 
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
+
 import android.content.ComponentName;
 import android.content.pm.ShortcutInfo;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.LauncherIcons;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import java.util.stream.Collectors;
 
 /**
  * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
@@ -122,7 +125,11 @@
         return filteredShortcuts;
     }
 
-    public static Runnable createUpdateRunnable(final Launcher launcher, final ItemInfo originalInfo,
+    /**
+     * Returns a runnable to update the provided shortcuts and notifications
+     */
+    public static Runnable createUpdateRunnable(final BaseDraggingActivity launcher,
+            final ItemInfo originalInfo,
             final Handler uiHandler, final PopupContainerWithArrow container,
             final List<DeepShortcutView> shortcutViews,
             final List<NotificationKeyData> notificationKeys) {
@@ -130,38 +137,36 @@
         final UserHandle user = originalInfo.user;
         return () -> {
             if (!notificationKeys.isEmpty()) {
-                List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
-                        .getStatusBarNotificationsForKeys(notificationKeys);
-                List<NotificationInfo> infos = new ArrayList<>(notifications.size());
-                for (int i = 0; i < notifications.size(); i++) {
-                    StatusBarNotification notification = notifications.get(i);
-                    infos.add(new NotificationInfo(launcher, notification));
+                NotificationListener notificationListener =
+                        NotificationListener.getInstanceIfConnected();
+                final List<NotificationInfo> infos;
+                if (notificationListener == null) {
+                    infos = Collections.emptyList();
+                } else {
+                    infos = notificationListener.getNotificationsForKeys(notificationKeys).stream()
+                            .map(sbn -> new NotificationInfo(launcher, sbn, originalInfo))
+                            .collect(Collectors.toList());
                 }
                 uiHandler.post(() -> container.applyNotificationInfos(infos));
             }
 
-            List<ShortcutInfo> shortcuts = DeepShortcutManager.getInstance(launcher)
-                    .queryForShortcutsContainer(activity, user);
+            List<ShortcutInfo> shortcuts = new ShortcutRequest(launcher, user)
+                    .withContainer(activity)
+                    .query(ShortcutRequest.PUBLISHED);
             String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
                     : notificationKeys.get(0).shortcutId;
             shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
+            IconCache cache = LauncherAppState.getInstance(launcher).getIconCache();
             for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
                 final ShortcutInfo shortcut = shortcuts.get(i);
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, launcher);
-                // Use unbadged icon for the menu.
-                LauncherIcons li = LauncherIcons.obtain(launcher);
-                si.applyFrom(li.createShortcutIcon(shortcut, false /* badged */));
-                li.recycle();
+                cache.getUnbadgedShortcutIcon(si, shortcut);
                 si.rank = i;
+                si.container = CONTAINER_SHORTCUTS;
 
                 final DeepShortcutView view = shortcutViews.get(i);
                 uiHandler.post(() -> view.applyShortcutInfo(si, shortcut, container));
             }
-
-            // This ensures that mLauncher.getWidgetsForPackageUser()
-            // doesn't return null (it puts all the widgets in memory).
-            uiHandler.post(() -> launcher.refreshAndBindWidgetsForPackageUser(
-                    PackageUserKey.fromItemInfo(originalInfo)));
         };
     }
 }
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 5a5fbab..61829c0 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,71 +16,100 @@
 
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.annotation.TargetApi;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
+import android.content.Context;
 import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.Build;
 import android.util.Log;
 import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 
+@TargetApi(Build.VERSION_CODES.Q)
 public class RemoteActionShortcut extends SystemShortcut<BaseDraggingActivity> {
     private static final String TAG = "RemoteActionShortcut";
     private static final boolean DEBUG = Utilities.IS_DEBUG_DEVICE;
 
     private final RemoteAction mAction;
 
-    public RemoteActionShortcut(RemoteAction action) {
-        super(action.getIcon(), action.getTitle(), action.getContentDescription(),
-                R.id.action_remote_action_shortcut);
+    public RemoteActionShortcut(RemoteAction action,
+            BaseDraggingActivity activity, ItemInfo itemInfo) {
+        super(0, R.id.action_remote_action_shortcut, activity, itemInfo);
         mAction = action;
     }
 
     @Override
-    public View.OnClickListener getOnClickListener(
-            final BaseDraggingActivity activity, final ItemInfo itemInfo) {
-        return view -> {
-            AbstractFloatingView.closeAllOpenViews(activity);
+    public void setIconAndLabelFor(View iconView, TextView labelView) {
+        mAction.getIcon().loadDrawableAsync(iconView.getContext(),
+                iconView::setBackground,
+                MAIN_EXECUTOR.getHandler());
+        labelView.setText(mAction.getTitle());
+    }
 
-            final String actionIdentity = mAction.getTitle() + ", " +
-                    itemInfo.getTargetComponent().getPackageName();
-            try {
-                if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
-                mAction.getActionIntent().send(
-                        activity,
-                        0,
-                        new Intent().putExtra(
-                                Intent.EXTRA_PACKAGE_NAME,
-                                itemInfo.getTargetComponent().getPackageName()),
-                        (pendingIntent, intent, resultCode, resultData, resultExtras) -> {
-                            if (DEBUG) Log.d(TAG, "Action is complete: " + actionIdentity);
-                            if (resultData != null && !resultData.isEmpty()) {
-                                Log.e(TAG, "Remote action returned result: " + actionIdentity
-                                        + " : " + resultData);
-                                Toast.makeText(activity, resultData, Toast.LENGTH_SHORT).show();
-                            }
-                        },
-                        new Handler(Looper.getMainLooper()));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
-                Toast.makeText(activity, activity.getString(
-                        R.string.remote_action_failed,
-                        mAction.getTitle()),
-                        Toast.LENGTH_SHORT)
-                        .show();
-            }
+    @Override
+    public void setIconAndContentDescriptionFor(ImageView view) {
+        mAction.getIcon().loadDrawableAsync(view.getContext(),
+                view::setImageDrawable,
+                MAIN_EXECUTOR.getHandler());
+        view.setContentDescription(mAction.getContentDescription());
+    }
 
-            activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
-                    LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view);
-        };
+    @Override
+    public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
+        return new AccessibilityNodeInfo.AccessibilityAction(
+                R.id.action_remote_action_shortcut, mAction.getContentDescription());
+    }
+
+    @Override
+    public void onClick(View view) {
+        AbstractFloatingView.closeAllOpenViews(mTarget);
+        mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
+                .log(LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP);
+
+        final String actionIdentity = mAction.getTitle() + ", "
+                + mItemInfo.getTargetComponent().getPackageName();
+        try {
+            if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
+            mAction.getActionIntent().send(
+                    mTarget,
+                    0,
+                    new Intent().putExtra(
+                            Intent.EXTRA_PACKAGE_NAME,
+                            mItemInfo.getTargetComponent().getPackageName()),
+                    (pendingIntent, intent, resultCode, resultData, resultExtras) -> {
+                        if (DEBUG) Log.d(TAG, "Action is complete: " + actionIdentity);
+                        if (resultData != null && !resultData.isEmpty()) {
+                            Log.e(TAG, "Remote action returned result: " + actionIdentity
+                                    + " : " + resultData);
+                            Toast.makeText(mTarget, resultData, Toast.LENGTH_SHORT).show();
+                        }
+                    },
+                    MAIN_EXECUTOR.getHandler());
+        } catch (PendingIntent.CanceledException e) {
+            Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
+            Toast.makeText(mTarget, mTarget.getString(
+                    R.string.remote_action_failed,
+                    mAction.getTitle()),
+                    Toast.LENGTH_SHORT)
+                    .show();
+        }
+
+        mTarget.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
+                LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view);
     }
 
     @Override
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index a87b7b8..fd292a3 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,30 +1,27 @@
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
 
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.graphics.drawable.Icon;
-import android.os.Handler;
-import android.os.Looper;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -32,6 +29,7 @@
 import com.android.launcher3.widget.WidgetsBottomSheet;
 
 import java.util.List;
+
 /**
  * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
  * onClickListener that depends on the item that the shortcut services.
@@ -39,41 +37,30 @@
  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
  * @param <T>
  */
-public abstract class SystemShortcut<T extends BaseDraggingActivity>
-        extends ItemInfo {
+public abstract class SystemShortcut<T extends BaseDraggingActivity> extends ItemInfo
+        implements View.OnClickListener {
+
     private final int mIconResId;
     private final int mLabelResId;
-    private final Icon mIcon;
-    private final CharSequence mLabel;
-    private final CharSequence mContentDescription;
     private final int mAccessibilityActionId;
 
-    public SystemShortcut(int iconResId, int labelResId) {
+    protected final T mTarget;
+    protected final ItemInfo mItemInfo;
+
+    public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
         mIconResId = iconResId;
         mLabelResId = labelResId;
         mAccessibilityActionId = labelResId;
-        mIcon = null;
-        mLabel = null;
-        mContentDescription = null;
+        mTarget = target;
+        mItemInfo = itemInfo;
     }
 
-    public SystemShortcut(Icon icon, CharSequence label, CharSequence contentDescription,
-            int accessibilityActionId) {
-        mIcon = icon;
-        mLabel = label;
-        mContentDescription = contentDescription;
-        mAccessibilityActionId = accessibilityActionId;
-        mIconResId = 0;
-        mLabelResId = 0;
-    }
-
-    public SystemShortcut(SystemShortcut other) {
+    public SystemShortcut(SystemShortcut<T> other) {
         mIconResId = other.mIconResId;
         mLabelResId = other.mLabelResId;
-        mIcon = other.mIcon;
-        mLabel = other.mLabel;
-        mContentDescription = other.mContentDescription;
         mAccessibilityActionId = other.mAccessibilityActionId;
+        mTarget = other.mTarget;
+        mItemInfo = other.mItemInfo;
     }
 
     /**
@@ -84,150 +71,115 @@
     }
 
     public void setIconAndLabelFor(View iconView, TextView labelView) {
-        if (mIcon != null) {
-            mIcon.loadDrawableAsync(iconView.getContext(),
-                    iconView::setBackground,
-                    new Handler(Looper.getMainLooper()));
-        } else {
-            iconView.setBackgroundResource(mIconResId);
-        }
-
-        if (mLabel != null) {
-            labelView.setText(mLabel);
-        } else {
-            labelView.setText(mLabelResId);
-        }
+        iconView.setBackgroundResource(mIconResId);
+        labelView.setText(mLabelResId);
     }
 
     public void setIconAndContentDescriptionFor(ImageView view) {
-        if (mIcon != null) {
-            mIcon.loadDrawableAsync(view.getContext(),
-                    view::setImageDrawable,
-                    new Handler(Looper.getMainLooper()));
-        } else {
-            view.setImageResource(mIconResId);
-        }
-
-        view.setContentDescription(getContentDescription(view.getContext()));
-    }
-
-    private CharSequence getContentDescription(Context context) {
-        return mContentDescription != null ? mContentDescription : context.getText(mLabelResId);
+        view.setImageResource(mIconResId);
+        view.setContentDescription(view.getContext().getText(mLabelResId));
     }
 
     public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
-        return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId,
-                getContentDescription(context));
+        return new AccessibilityNodeInfo.AccessibilityAction(
+                mAccessibilityActionId, context.getText(mLabelResId));
     }
 
     public boolean hasHandlerForAction(int action) {
         return mAccessibilityActionId == action;
     }
 
-    public abstract View.OnClickListener getOnClickListener(T activity, ItemInfo itemInfo);
+    public interface Factory<T extends BaseDraggingActivity> {
+
+        @Nullable SystemShortcut<T> getShortcut(T activity, ItemInfo itemInfo);
+    }
+
+    public static final Factory<Launcher> WIDGETS = (launcher, itemInfo) -> {
+        if (itemInfo.getTargetComponent() == null) return null;
+        final List<WidgetItem> widgets =
+                launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
+                        itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
+        if (widgets == null) {
+            return null;
+        }
+        return new Widgets(launcher, itemInfo);
+    };
 
     public static class Widgets extends SystemShortcut<Launcher> {
-
-        public Widgets() {
-            super(R.drawable.ic_widget, R.string.widget_button_text);
+        public Widgets(Launcher target, ItemInfo itemInfo) {
+            super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(final Launcher launcher,
-                final ItemInfo itemInfo) {
-            if (itemInfo.getTargetComponent() == null) return null;
-            final List<WidgetItem> widgets =
-                    launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
-                            itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
-            if (widgets == null) {
-                return null;
-            }
-            return (view) -> {
-                AbstractFloatingView.closeAllOpenViews(launcher);
-                WidgetsBottomSheet widgetsBottomSheet =
-                        (WidgetsBottomSheet) launcher.getLayoutInflater().inflate(
-                                R.layout.widgets_bottom_sheet, launcher.getDragLayer(), false);
-                widgetsBottomSheet.populateAndShow(itemInfo);
-                launcher.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.WIDGETS_BUTTON, view);
-            };
+        public void onClick(View view) {
+            AbstractFloatingView.closeAllOpenViews(mTarget);
+            WidgetsBottomSheet widgetsBottomSheet =
+                    (WidgetsBottomSheet) mTarget.getLayoutInflater().inflate(
+                            R.layout.widgets_bottom_sheet, mTarget.getDragLayer(), false);
+            widgetsBottomSheet.populateAndShow(mItemInfo);
+            mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                    ControlType.WIDGETS_BUTTON, view);
+            mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
+                    .log(LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP);
         }
     }
 
+    public static final Factory<BaseDraggingActivity> APP_INFO = AppInfo::new;
+
     public static class AppInfo extends SystemShortcut {
-        public AppInfo() {
-            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label);
+
+        public AppInfo(BaseDraggingActivity target, ItemInfo itemInfo) {
+            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target,
+                    itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            return (view) -> {
-                dismissTaskMenuView(activity);
-                Rect sourceBounds = activity.getViewBounds(view);
-                new PackageManagerHelper(activity).startDetailsActivityForInfo(
-                        itemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle());
-                activity.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.APPINFO_TARGET, view);
-            };
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            Rect sourceBounds = mTarget.getViewBounds(view);
+            new PackageManagerHelper(mTarget).startDetailsActivityForInfo(
+                    mItemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle());
+            mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                    ControlType.APPINFO_TARGET, view);
+            mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
+                    .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP);
         }
     }
 
+    public static final Factory<BaseDraggingActivity> INSTALL = (activity, itemInfo) -> {
+        boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo)
+                && ((WorkspaceItemInfo) itemInfo).hasStatusFlag(
+                        WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI);
+        boolean isInstantApp = false;
+        if (itemInfo instanceof com.android.launcher3.model.data.AppInfo) {
+            com.android.launcher3.model.data.AppInfo
+                    appInfo = (com.android.launcher3.model.data.AppInfo) itemInfo;
+            isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo);
+        }
+        boolean enabled = supportsWebUI || isInstantApp;
+        if (!enabled) {
+            return null;
+        }
+        return new Install(activity, itemInfo);
+    };
+
     public static class Install extends SystemShortcut {
-        public Install() {
-            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label);
+
+        public Install(BaseDraggingActivity target, ItemInfo itemInfo) {
+            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label,
+                    target, itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) &&
-                    ((WorkspaceItemInfo) itemInfo).hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI);
-            boolean isInstantApp = false;
-            if (itemInfo instanceof com.android.launcher3.AppInfo) {
-                com.android.launcher3.AppInfo appInfo = (com.android.launcher3.AppInfo) itemInfo;
-                isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo);
-            }
-            boolean enabled = supportsWebUI || isInstantApp;
-            if (!enabled) {
-                return null;
-            }
-            return createOnClickListener(activity, itemInfo);
-        }
-
-        public View.OnClickListener createOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            return view -> {
-                Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
-                        itemInfo.getTargetComponent().getPackageName());
-                activity.startActivitySafely(view, intent, itemInfo, null);
-                AbstractFloatingView.closeAllOpenViews(activity);
-            };
+        public void onClick(View view) {
+            Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
+                    mItemInfo.getTargetComponent().getPackageName());
+            mTarget.startActivitySafely(view, intent, mItemInfo, null);
+            AbstractFloatingView.closeAllOpenViews(mTarget);
         }
     }
 
-    public static class DismissPrediction extends SystemShortcut<Launcher> {
-        public DismissPrediction() {
-            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label);
-        }
-
-        @Override
-        public View.OnClickListener getOnClickListener(Launcher activity, ItemInfo itemInfo) {
-            if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
-            if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
-            return (view) -> {
-                PopupContainerWithArrow.closeAllOpenViews(activity);
-                activity.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.DISMISS_PREDICTION, ContainerType.DEEPSHORTCUTS);
-                AppLaunchTracker.INSTANCE.get(view.getContext())
-                        .onDismissApp(itemInfo.getTargetComponent(),
-                                itemInfo.user,
-                                AppLaunchTracker.CONTAINER_PREDICTIONS);
-            };
-        }
-    }
-
-    protected static void dismissTaskMenuView(BaseDraggingActivity activity) {
+    public static void dismissTaskMenuView(BaseDraggingActivity activity) {
         AbstractFloatingView.closeOpenViews(activity, true,
             AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
     }
diff --git a/src/com/android/launcher3/popup/SystemShortcutFactory.java b/src/com/android/launcher3/popup/SystemShortcutFactory.java
deleted file mode 100644
index dfcc2f8..0000000
--- a/src/com/android/launcher3/popup/SystemShortcutFactory.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.popup;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class SystemShortcutFactory implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<SystemShortcutFactory> INSTANCE =
-            forOverride(SystemShortcutFactory.class, R.string.system_shortcut_factory_class);
-
-    /** Note that these are in order of priority. */
-    private final SystemShortcut[] mAllShortcuts;
-
-    @SuppressWarnings("unused")
-    public SystemShortcutFactory() {
-        this(new SystemShortcut.AppInfo(),
-                new SystemShortcut.Widgets(),
-                new SystemShortcut.Install(),
-                new SystemShortcut.DismissPrediction());
-    }
-
-    protected SystemShortcutFactory(SystemShortcut... shortcuts) {
-        mAllShortcuts = shortcuts;
-    }
-
-    public @NonNull List<SystemShortcut> getEnabledShortcuts(Launcher launcher, ItemInfo info) {
-        List<SystemShortcut> systemShortcuts = new ArrayList<>();
-        for (SystemShortcut systemShortcut : mAllShortcuts) {
-            if (systemShortcut.getOnClickListener(launcher, info) != null) {
-                systemShortcuts.add(systemShortcut);
-            }
-        }
-
-        return systemShortcuts;
-    }
-}
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
index 970a03e..a5462a6 100644
--- a/src/com/android/launcher3/provider/ImportDataTask.java
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -37,16 +37,16 @@
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.DefaultLayoutParser;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.GridSizeMigrationTask;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -100,7 +100,7 @@
      * 3) In the end fills any holes in hotseat with items from default hotseat layout.
      */
     private void importWorkspaceItems() throws Exception {
-        String profileId = Long.toString(UserManagerCompat.getInstance(mContext)
+        String profileId = Long.toString(UserCache.INSTANCE.get(mContext)
                 .getSerialNumberForUser(Process.myUserHandle()));
 
         boolean createEmptyRowOnFirstScreen;
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 2c843f9..7e05a5a 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -22,10 +22,12 @@
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.Binder;
+import android.os.Process;
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.IntArray;
 
 import java.util.Locale;
@@ -116,6 +118,23 @@
         db.execSQL("DROP TABLE IF EXISTS " + tableName);
     }
 
+    /** Copy fromTable in fromDb to toTable in toDb. */
+    public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
+            String toTable, Context context) {
+        long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
+                Process.myUserHandle());
+        dropTable(toDb, toTable);
+        Favorites.addTableToDb(toDb, userSerial, false, toTable);
+        if (fromDb != toDb) {
+            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
+            toDb.execSQL(
+                    "INSERT INTO " + toTable + " SELECT * FROM from_db." + fromTable);
+            toDb.execSQL("DETACH DATABASE 'from_db'");
+        } else {
+            toDb.execSQL("INSERT INTO " + toTable + " SELECT * FROM " + fromTable);
+        }
+    }
+
     /**
      * Utility class to simplify managing sqlite transactions
      */
diff --git a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
index 6d839f3..c0111b9 100644
--- a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
+++ b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
@@ -18,7 +18,6 @@
 
 import android.content.ContentValues;
 import android.content.Context;
-import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
@@ -43,7 +42,7 @@
     protected LossyScreenMigrationTask(
             Context context, InvariantDeviceProfile idp, SQLiteDatabase db) {
         // Decrease the rows count by 1
-        super(context, db, getValidPackages(context),
+        super(context, db, getValidPackages(context), false /* usePreviewTable */,
                 new Point(idp.numColumns, idp.numRows + 1),
                 new Point(idp.numColumns, idp.numRows));
 
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index fb33551..53183bf 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -25,23 +25,28 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.LongSparseArray;
 import android.util.SparseLongArray;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.AppWidgetsRestoredReceiver;
-import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.GridBackupTable;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 
 import java.io.InvalidObjectException;
+import java.util.Arrays;
 
 /**
  * Utility class to update DB schema after it has been restored.
@@ -65,6 +70,7 @@
         SQLiteDatabase db = helper.getWritableDatabase();
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
+            task.backupWorkspace(context, db);
             task.sanitizeDB(helper, db, backupManager);
             task.restoreAppWidgetIdsIfExists(context);
             t.commit();
@@ -76,14 +82,59 @@
     }
 
     /**
+     * Restore the workspace if backup is available.
+     */
+    public static boolean restoreIfPossible(@NonNull Context context,
+            @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) {
+        final SQLiteDatabase db = helper.getWritableDatabase();
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            RestoreDbTask task = new RestoreDbTask();
+            task.restoreWorkspace(context, db, helper, backupManager);
+            t.commit();
+            return true;
+        } catch (Exception e) {
+            FileLog.e(TAG, "Failed to restore db", e);
+            return false;
+        }
+    }
+
+    /**
+     * Backup the workspace so that if things go south in restore, we can recover these entries.
+     */
+    private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
+                .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
+    }
+
+    private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db,
+            @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
+            throws Exception {
+        final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        GridBackupTable backupTable = new GridBackupTable(context, db, idp.numHotseatIcons,
+                idp.numColumns, idp.numRows);
+        if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
+            int itemsDeleted = sanitizeDB(helper, db, backupManager);
+            LauncherAppState.getInstance(context).getModel().forceReload();
+            restoreAppWidgetIdsIfExists(context);
+            if (itemsDeleted == 0) {
+                // all the items are restored, we no longer need the backup table
+                dropTable(db, Favorites.BACKUP_TABLE_NAME);
+            }
+        }
+    }
+
+    /**
      * Makes the following changes in the provider DB.
      *   1. Removes all entries belonging to any profiles that were not restored.
      *   2. Marks all entries as restored. The flags are updated during first load or as
      *      the restored apps get installed.
      *   3. If the user serial for any restored profile is different than that of the previous
      *      device, update the entries to the new profile id.
+     *
+     * @return number of items deleted.
      */
-    private void sanitizeDB(DatabaseHelper helper, SQLiteDatabase db, BackupManager backupManager)
+    private int sanitizeDB(DatabaseHelper helper, SQLiteDatabase db, BackupManager backupManager)
             throws Exception {
         // Primary user ids
         long myProfileId = helper.getDefaultUserSerial();
@@ -107,15 +158,14 @@
         int numProfiles = profileMapping.size();
         String[] profileIds = new String[numProfiles];
         profileIds[0] = Long.toString(oldProfileId);
-        StringBuilder whereClause = new StringBuilder("profileId != ?");
-        for (int i = profileMapping.size() - 1; i >= 1; --i) {
-            whereClause.append(" AND profileId != ?");
+        for (int i = numProfiles - 1; i >= 1; --i) {
             profileIds[i] = Long.toString(profileMapping.keyAt(i));
         }
-        int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
-        if (itemsDeleted > 0) {
-            FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
-        }
+        final String[] args = new String[profileIds.length];
+        Arrays.fill(args, "?");
+        final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
+        int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
+        FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
 
         // Mark all items as restored.
         boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
@@ -125,15 +175,16 @@
         db.update(Favorites.TABLE_NAME, values, null, null);
 
         // Mark widgets with appropriate restore flag.
-        values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
-                LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
-                LauncherAppWidgetInfo.FLAG_UI_NOT_READY |
-                (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
+        values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
+                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+                | LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+                | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
         db.update(Favorites.TABLE_NAME, values, "itemType = ?",
                 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
 
-        // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp location.
-        // Using Long.MIN_VALUE since profile ids can not be negative, so there will be no overlap.
+        // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp
+        // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will
+        // be no overlap.
         final long tempLocationOffset = Long.MIN_VALUE;
         SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size());
         int numTempMigrations = 0;
@@ -160,6 +211,7 @@
         if (myProfileId != oldProfileId) {
             changeDefaultColumn(db, myProfileId);
         }
+        return itemsDeleted;
     }
 
     /**
@@ -191,10 +243,10 @@
     private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) {
         LongSparseArray<Long> ids = new LongSparseArray<>();
         try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? "
-                + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})){
-                while (c.moveToNext()) {
-                    ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
-                }
+                + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) {
+            while (c.moveToNext()) {
+                ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
+            }
         }
         return ids;
     }
@@ -215,7 +267,7 @@
      * Returns the profile id used in the favorites table of the provided db.
      */
     protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
-        try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+        try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) {
             int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
             while (c.moveToNext()) {
                 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 0eb4285..289e0d8 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -54,7 +54,7 @@
  * 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)}.
  *
- * Note: AppWidgetManagerCompat can be disabled using FeatureFlags. In QSB, we should use
+ * Note: WidgetManagerHelper can be disabled using FeatureFlags. In QSB, we should use
  * AppWidgetManager directly, so that it keeps working in that case.
  */
 public class QsbContainerView extends FrameLayout {
diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
new file mode 100644
index 0000000..e9058c3
--- /dev/null
+++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
@@ -0,0 +1,234 @@
+/*
+ * 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.secondarydisplay;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.ComponentName;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Process;
+import android.os.UserHandle;
+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.BaseAdapter;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.allapps.AppInfoComparator;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Executors;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Adapter to manage pinned apps and show then in a grid.
+ */
+public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreferenceChangeListener {
+
+    private static final String PINNED_APPS_KEY = "pinned_apps";
+
+    private final SecondaryDisplayLauncher mLauncher;
+    private final OnClickListener mOnClickListener;
+    private final OnLongClickListener mOnLongClickListener;
+    private final SharedPreferences mPrefs;
+    private final AllAppsStore mAllAppsList;
+    private final AppInfoComparator mAppNameComparator;
+
+    private final Set<ComponentKey> mPinnedApps = new HashSet<>();
+    private final ArrayList<AppInfo> mItems = new ArrayList<>();
+
+    public PinnedAppsAdapter(SecondaryDisplayLauncher launcher, AllAppsStore allAppsStore,
+            OnLongClickListener onLongClickListener) {
+        mLauncher = launcher;
+        mOnClickListener = launcher.getItemOnClickListener();
+        mOnLongClickListener = onLongClickListener;
+        mAllAppsList = allAppsStore;
+        mPrefs = launcher.getSharedPreferences(PINNED_APPS_KEY, MODE_PRIVATE);
+        mAppNameComparator = new AppInfoComparator(launcher);
+
+        mAllAppsList.addUpdateListener(this::createFilteredAppsList);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        if (PINNED_APPS_KEY.equals(key)) {
+            Executors.MODEL_EXECUTOR.submit(() -> {
+                Set<ComponentKey> apps = prefs.getStringSet(key, Collections.emptySet())
+                        .stream()
+                        .map(this::parseComponentKey)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+                Executors.MAIN_EXECUTOR.submit(() -> {
+                    mPinnedApps.clear();
+                    mPinnedApps.addAll(apps);
+                    createFilteredAppsList();
+                });
+            });
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getCount() {
+        return mItems.size();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AppInfo getItem(int position) {
+        return mItems.get(position);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View getView(int position, View view, ViewGroup parent) {
+        BubbleTextView icon;
+        if (view instanceof BubbleTextView) {
+            icon = (BubbleTextView) view;
+        } else {
+            icon = (BubbleTextView) LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.app_icon, parent, false);
+            icon.setOnClickListener(mOnClickListener);
+            icon.setOnLongClickListener(mOnLongClickListener);
+            icon.setLongPressTimeoutFactor(1f);
+            int padding = mLauncher.getDeviceProfile().edgeMarginPx;
+            icon.setPadding(padding, padding, padding, padding);
+        }
+
+        icon.applyFromApplicationInfo(mItems.get(position));
+        return icon;
+    }
+
+    private void createFilteredAppsList() {
+        mItems.clear();
+        mPinnedApps.stream().map(mAllAppsList::getApp)
+                .filter(Objects::nonNull).forEach(mItems::add);
+        mItems.sort(mAppNameComparator);
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Initialized the pinned apps list and starts listening for changes
+     */
+    public void init() {
+        mPrefs.registerOnSharedPreferenceChangeListener(this);
+        onSharedPreferenceChanged(mPrefs, PINNED_APPS_KEY);
+    }
+
+    /**
+     * Stops listening for any pinned apps changes
+     */
+    public void destroy() {
+        mPrefs.unregisterOnSharedPreferenceChangeListener(this);
+    }
+
+    private void update(ItemInfo info, Function<ComponentKey, Boolean> op) {
+        ComponentKey key = new ComponentKey(info.getTargetComponent(), info.user);
+        if (op.apply(key)) {
+            createFilteredAppsList();
+            Set<ComponentKey> copy = new HashSet<>(mPinnedApps);
+            Executors.MODEL_EXECUTOR.submit(() ->
+                    mPrefs.edit().putStringSet(PINNED_APPS_KEY,
+                        copy.stream().map(this::encode).collect(Collectors.toSet()))
+                        .apply());
+        }
+    }
+
+    private ComponentKey parseComponentKey(String string) {
+        try {
+            String[] parts = string.split("#");
+            UserHandle user;
+            if (parts.length > 2) {
+                user = UserCache.INSTANCE.get(mLauncher)
+                        .getUserForSerialNumber(Long.parseLong(parts[2]));
+            } else {
+                user = Process.myUserHandle();
+            }
+            ComponentName cn = ComponentName.unflattenFromString(parts[0]);
+            return new ComponentKey(cn, user);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String encode(ComponentKey key) {
+        return key.componentName.flattenToShortString() + "#"
+                + UserCache.INSTANCE.get(mLauncher).getSerialNumberForUser(key.user);
+    }
+
+    /**
+     * Returns a system shortcut to pin/unpin a shortcut
+     */
+    public SystemShortcut getSystemShortcut(ItemInfo info) {
+        return new PinUnPinShortcut(mLauncher, info,
+                mPinnedApps.contains(new ComponentKey(info.getTargetComponent(), info.user)));
+    }
+
+    private class PinUnPinShortcut extends SystemShortcut<SecondaryDisplayLauncher> {
+
+        private final boolean mIsPinned;
+
+        PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, boolean isPinned) {
+            super(isPinned ? R.drawable.ic_remove_no_shadow : R.drawable.ic_pin,
+                    isPinned ? R.string.remove_drop_target_label : R.string.action_add_to_workspace,
+                    target, info);
+            mIsPinned = isPinned;
+        }
+
+        @Override
+        public void onClick(View view) {
+            if (mIsPinned) {
+                update(mItemInfo, mPinnedApps::remove);
+            } else {
+                update(mItemInfo, mPinnedApps::add);
+            }
+            AbstractFloatingView.closeAllOpenViews(mLauncher);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
new file mode 100644
index 0000000..21ad275
--- /dev/null
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -0,0 +1,333 @@
+/*
+ * 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.secondarydisplay;
+
+import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityOptions;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewAnimationUtils;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.model.BgDataModel;
+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.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.ItemInfoMatcher;
+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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Launcher activity for secondary displays
+ */
+public class SecondaryDisplayLauncher extends BaseDraggingActivity
+        implements BgDataModel.Callbacks {
+
+    private LauncherModel mModel;
+
+    private BaseDragLayer mDragLayer;
+    private AllAppsContainerView mAppsView;
+    private View mAppsButton;
+
+    private PopupDataProvider mPopupDataProvider;
+
+    private boolean mAppDrawerShown = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mModel = LauncherAppState.getInstance(this).getModel();
+        if (getWindow().getDecorView().isAttachedToWindow()) {
+            initUi();
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        initUi();
+    }
+
+    private void initUi() {
+        if (mDragLayer != null) {
+            return;
+        }
+        InvariantDeviceProfile currentDisplayIdp =
+                new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay());
+
+        // Disable transpose layout and use multi-window mode so that the icons are scaled properly
+        mDeviceProfile = currentDisplayIdp.getDeviceProfile(this)
+                .toBuilder(this)
+                .setMultiWindowMode(true)
+                .setTransposeLayoutWithOrientation(false)
+                .build();
+        mDeviceProfile.autoResizeAllAppsCells();
+
+        setContentView(R.layout.secondary_launcher);
+        mDragLayer = findViewById(R.id.drag_layer);
+        mAppsView = findViewById(R.id.apps_view);
+        mAppsButton = findViewById(R.id.all_apps_button);
+
+        mPopupDataProvider = new PopupDataProvider(
+                mAppsView.getAppsStore()::updateNotificationDots);
+
+        mModel.addCallbacksAndLoad(this);
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+
+        if (Intent.ACTION_MAIN.equals(intent.getAction())) {
+            // Hide keyboard.
+            final View v = getWindow().peekDecorView();
+            if (v != null && v.getWindowToken() != null) {
+                getSystemService(InputMethodManager.class).hideSoftInputFromWindow(
+                        v.getWindowToken(), 0);
+            }
+        }
+
+        // A new intent will bring the launcher to top. Hide the app drawer to reset the state.
+        showAppDrawer(false);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (finishAutoCancelActionMode()) {
+            return;
+        }
+
+        // Note: There should be at most one log per method call. This is enforced implicitly
+        // by using if-else statements.
+        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this);
+        if (topView != null && topView.onBackPressed()) {
+            // Handled by the floating view.
+        } else {
+            showAppDrawer(false);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mModel.removeCallbacks(this);
+    }
+
+    public boolean isAppDrawerShown() {
+        return mAppDrawerShown;
+    }
+
+    public AllAppsContainerView getAppsView() {
+        return mAppsView;
+    }
+
+    @Override
+    public <T extends View> T getOverviewPanel() {
+        return null;
+    }
+
+    @Override
+    public View getRootView() {
+        return mDragLayer;
+    }
+
+    @Override
+    public ActivityOptions getActivityLaunchOptions(View v) {
+        return null;
+    }
+
+    @Override
+    protected void reapplyUi() { }
+
+    @Override
+    public BaseDragLayer getDragLayer() {
+        return mDragLayer;
+    }
+
+    @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 bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
+    @Override
+    public void bindScreens(IntArray orderedScreenIds) { }
+
+    @Override
+    public void finishFirstPageBind(ViewOnDrawExecutor executor) {
+        if (executor != null) {
+            executor.onLoadAnimationCompleted();
+        }
+    }
+
+    @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 bindPromiseAppProgressUpdated(PromiseAppInfo app) {
+        mAppsView.getAppsStore().updatePromiseAppProgress(app);
+    }
+
+    @Override
+    public void bindWorkspaceItemsChanged(ArrayList<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(ArrayList<WidgetListRowEntry> widgets) { }
+
+    @Override
+    public void onPageBoundSynchronously(int page) { }
+
+    @Override
+    public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+        executor.attachTo(getDragLayer(), false, null);
+    }
+
+    /**
+     * Called when apps-button is clicked
+     */
+    public void onAppsButtonClicked(View v) {
+        showAppDrawer(true);
+    }
+
+    /**
+     * Show/hide app drawer card with animation.
+     */
+    public void showAppDrawer(boolean show) {
+        if (show == mAppDrawerShown) {
+            return;
+        }
+
+        float openR = (float) Math.hypot(mAppsView.getWidth(), mAppsView.getHeight());
+        float closeR = Themes.getDialogCornerRadius(this);
+        float startR = mAppsButton.getWidth() / 2f;
+
+        float[] buttonPos = new float[] { startR, startR};
+        mDragLayer.getDescendantCoordRelativeToSelf(mAppsButton, buttonPos);
+        mDragLayer.mapCoordInSelfToDescendant(mAppsView, buttonPos);
+        final Animator animator = ViewAnimationUtils.createCircularReveal(mAppsView,
+                (int) buttonPos[0], (int) buttonPos[1],
+                show ? closeR : openR, show ? openR : closeR);
+
+        if (show) {
+            mAppDrawerShown = true;
+            mAppsView.setVisibility(View.VISIBLE);
+            mAppsButton.setVisibility(View.INVISIBLE);
+        } else {
+            mAppDrawerShown = false;
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAppsView.setVisibility(View.INVISIBLE);
+                    mAppsButton.setVisibility(View.VISIBLE);
+                    mAppsView.getSearchUiManager().resetSearch();
+                }
+            });
+        }
+        animator.start();
+    }
+
+    @Override
+    public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) {
+        mPopupDataProvider.setDeepShortcutMap(deepShortcutMap);
+    }
+
+    @Override
+    public void bindAllApplications(AppInfo[] apps, int flags) {
+        mAppsView.getAppsStore().setApps(apps, flags);
+    }
+
+    public PopupDataProvider getPopupDataProvider() {
+        return mPopupDataProvider;
+    }
+
+    @Override
+    public OnClickListener getItemOnClickListener() {
+        return this::onIconClicked;
+    }
+
+    private void onIconClicked(View v) {
+        // Make sure that rogue clicks don't get through while allapps is launching, or after the
+        // view has detached (it's possible for this to happen if the view is removed mid touch).
+        if (v.getWindowToken() == null) return;
+
+        Object tag = v.getTag();
+        if (tag instanceof ItemInfo) {
+            ItemInfo item = (ItemInfo) tag;
+            Intent intent;
+            if (item instanceof PromiseAppInfo) {
+                PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
+                intent = promiseAppInfo.getMarketIntent(this);
+            } else {
+                intent = item.getIntent();
+            }
+            if (intent == null) {
+                throw new IllegalArgumentException("Input must have a valid intent");
+            }
+            startActivitySafely(v, intent, item, CONTAINER_ALL_APPS);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
new file mode 100644
index 0000000..40630d3
--- /dev/null
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -0,0 +1,195 @@
+/*
+ * 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.secondarydisplay;
+
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.GridView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.util.ShortcutUtil;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * DragLayer for Secondary launcher
+ */
+public class SecondaryDragLayer extends BaseDragLayer<SecondaryDisplayLauncher> {
+
+    private View mAllAppsButton;
+    private AllAppsContainerView mAppsView;
+
+    private GridView mWorkspace;
+    private PinnedAppsAdapter mPinnedAppsAdapter;
+
+    public SecondaryDragLayer(Context context, AttributeSet attrs) {
+        super(context, attrs, 1 /* alphaChannelCount */);
+        recreateControllers();
+    }
+
+    @Override
+    public void recreateControllers() {
+        mControllers = new TouchController[] {new CloseAllAppsTouchController()};
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mAllAppsButton = findViewById(R.id.all_apps_button);
+
+        mAppsView = findViewById(R.id.apps_view);
+        mAppsView.setOnIconLongClickListener(this::onIconLongClicked);
+
+        // Setup workspace
+        mWorkspace = findViewById(R.id.workspace_grid);
+        mPinnedAppsAdapter = new PinnedAppsAdapter(mActivity, mAppsView.getAppsStore(),
+                this::onIconLongClicked);
+        mWorkspace.setAdapter(mPinnedAppsAdapter);
+        mWorkspace.setNumColumns(mActivity.getDeviceProfile().inv.numColumns);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mPinnedAppsAdapter.init();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mPinnedAppsAdapter.destroy();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        setMeasuredDimension(width, height);
+
+        DeviceProfile grid = mActivity.getDeviceProfile();
+        InvariantDeviceProfile idp = grid.inv;
+
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child == mAppsView) {
+                int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx
+                        + grid.cellLayoutPaddingLeftRightPx);
+
+                int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
+                int appsWidth = Math.min(width, maxWidth);
+
+                int maxHeight = grid.allAppsCellHeightPx * idp.numAllAppsColumns + padding;
+                int appsHeight = Math.min(height, maxHeight);
+
+                mAppsView.measure(
+                        makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY));
+
+            } else if (child == mAllAppsButton) {
+                int appsButtonSpec = makeMeasureSpec(grid.iconSizePx, EXACTLY);
+                mAllAppsButton.measure(appsButtonSpec, appsButtonSpec);
+
+            } else if (child == mWorkspace) {
+                measureChildWithMargins(mWorkspace, widthMeasureSpec, 0, heightMeasureSpec,
+                        grid.iconSizePx + grid.edgeMarginPx);
+
+            } else {
+                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+            }
+        }
+    }
+
+    private class CloseAllAppsTouchController implements TouchController {
+
+        @Override
+        public boolean onControllerTouchEvent(MotionEvent ev) {
+            return false;
+        }
+
+        @Override
+        public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+            if (!mActivity.isAppDrawerShown()) {
+                return false;
+            }
+
+            if (AbstractFloatingView.getTopOpenView(mActivity) != null) {
+                return false;
+            }
+
+            if (ev.getAction() == MotionEvent.ACTION_DOWN
+                    && !isEventOverView(mActivity.getAppsView(), ev)) {
+                mActivity.showAppDrawer(false);
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private boolean onIconLongClicked(View v) {
+        if (!(v instanceof BubbleTextView)) {
+            return false;
+        }
+        if (PopupContainerWithArrow.getOpen(mActivity) != null) {
+            // There is already an items container open, so don't open this one.
+            v.clearFocus();
+            return false;
+        }
+        ItemInfo item = (ItemInfo) v.getTag();
+        if (!ShortcutUtil.supportsShortcuts(item)) {
+            return false;
+        }
+        final PopupContainerWithArrow container =
+                (PopupContainerWithArrow) mActivity.getLayoutInflater().inflate(
+                        R.layout.popup_container, mActivity.getDragLayer(), false);
+
+        container.populateAndShow((BubbleTextView) v,
+                mActivity.getPopupDataProvider().getShortcutCountForItem(item),
+                Collections.emptyList(),
+                Arrays.asList(mPinnedAppsAdapter.getSystemShortcut(item),
+                        APP_INFO.getShortcut(mActivity, item)));
+        v.getParent().requestDisallowInterceptTouchEvent(true);
+        return true;
+    }
+}
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index a9242f9..b12d04f 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.settings;
 
+import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
 
@@ -24,42 +26,45 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
+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;
 import android.provider.Settings;
 import android.util.ArrayMap;
-import android.util.ArraySet;
+import android.util.Pair;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceDataStore;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.PreferenceViewHolder;
+import androidx.preference.SwitchPreference;
+
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.config.FlagTogglerPrefUi;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceDataStore;
-import androidx.preference.PreferenceFragment;
-import androidx.preference.PreferenceScreen;
-import androidx.preference.PreferenceViewHolder;
-import androidx.preference.SwitchPreference;
+import java.util.stream.Collectors;
 
 /**
  * Dev-build only UI allowing developers to toggle flag settings and plugins.
  * See {@link FeatureFlags}.
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class DeveloperOptionsFragment extends PreferenceFragment {
+public class DeveloperOptionsFragment extends PreferenceFragmentCompat {
 
     private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS";
     private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
@@ -91,6 +96,7 @@
 
         initFlags();
         loadPluginPrefs();
+        maybeAddSandboxCategory();
     }
 
     @Override
@@ -154,44 +160,106 @@
         PackageManager pm = getContext().getPackageManager();
 
         Set<String> pluginActions = manager.getPluginActions();
-        ArrayMap<String, ArraySet<String>> plugins = new ArrayMap<>();
+
+        ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins =
+                new ArrayMap<>();
+
+        Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
+                new String[]{PLUGIN_PERMISSION}, MATCH_DISABLED_COMPONENTS)
+                .stream()
+                .map(pi -> pi.packageName)
+                .collect(Collectors.toSet());
+
         for (String action : pluginActions) {
             String name = toName(action);
             List<ResolveInfo> result = pm.queryIntentServices(
-                    new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS);
+                    new Intent(action), MATCH_DISABLED_COMPONENTS);
             for (ResolveInfo info : result) {
                 String packageName = info.serviceInfo.packageName;
-                if (!plugins.containsKey(packageName)) {
-                    plugins.put(packageName, new ArraySet<>());
+                if (!pluginPermissionApps.contains(packageName)) {
+                    continue;
                 }
-                plugins.get(packageName).add(name);
+
+                Pair<String, String> key = Pair.create(packageName, info.serviceInfo.processName);
+                if (!plugins.containsKey(key)) {
+                    plugins.put(key, new ArrayList<>());
+                }
+                plugins.get(key).add(Pair.create(name, info.serviceInfo));
             }
         }
 
-        List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION},
-                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
-        PreferenceDataStore enabled = manager.getPluginEnabler();
-        apps.forEach(app -> {
-            if (!plugins.containsKey(app.packageName)) return;
-            SwitchPreference pref = new PluginPreference(prefContext, app, enabled);
-            pref.setSummary("Plugins: " + toString(plugins.get(app.packageName)));
-            mPluginsCategory.addPreference(pref);
+        PreferenceDataStore enabler = manager.getPluginEnabler();
+        plugins.forEach((key, si) -> {
+            String packageName = key.first;
+            List<ComponentName> componentNames = si.stream()
+                    .map(p -> new ComponentName(packageName, p.second.name))
+                    .collect(Collectors.toList());
+            if (!componentNames.isEmpty()) {
+                SwitchPreference pref = new PluginPreference(
+                        prefContext, si.get(0).second.applicationInfo, enabler, componentNames);
+                pref.setSummary("Plugins: "
+                        + si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
+                mPluginsCategory.addPreference(pref);
+            }
         });
     }
 
-    private String toString(ArraySet<String> plugins) {
-        StringBuilder b = new StringBuilder();
-        for (String string : plugins) {
-            if (b.length() != 0) {
-                b.append(", ");
-            }
-            b.append(string);
+    private void maybeAddSandboxCategory() {
+        Context context = getContext();
+        if (context == null) {
+            return;
         }
-        return b.toString();
+        Intent launchSandboxIntent =
+                new Intent("com.android.quickstep.action.GESTURE_SANDBOX")
+                        .setPackage(context.getPackageName())
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        if (launchSandboxIntent.resolveActivity(context.getPackageManager()) == null) {
+            return;
+        }
+        PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox");
+        sandboxCategory.setSummary("Learn and practice navigation gestures");
+        Preference launchBackTutorialPreference = new Preference(context);
+        launchBackTutorialPreference.setKey("launchBackTutorial");
+        launchBackTutorialPreference.setTitle("Launch Back Tutorial");
+        launchBackTutorialPreference.setSummary("Learn how to use the Back gesture");
+        launchBackTutorialPreference.setOnPreferenceClickListener(preference -> {
+            startActivity(launchSandboxIntent.putExtra(
+                    "tutorial_type", "RIGHT_EDGE_BACK_NAVIGATION"));
+            return true;
+        });
+        sandboxCategory.addPreference(launchBackTutorialPreference);
+        Preference launchHomeTutorialPreference = new Preference(context);
+        launchHomeTutorialPreference.setKey("launchHomeTutorial");
+        launchHomeTutorialPreference.setTitle("Launch Home Tutorial");
+        launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture");
+        launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> {
+            startActivity(launchSandboxIntent.putExtra("tutorial_type", "HOME_NAVIGATION"));
+            return true;
+        });
+        sandboxCategory.addPreference(launchHomeTutorialPreference);
+        Preference launchOverviewTutorialPreference = new Preference(context);
+        launchOverviewTutorialPreference.setKey("launchOverviewTutorial");
+        launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial");
+        launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture");
+        launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> {
+            startActivity(launchSandboxIntent.putExtra("tutorial_type", "OVERVIEW_NAVIGATION"));
+            return true;
+        });
+        sandboxCategory.addPreference(launchOverviewTutorialPreference);
+        Preference launchAssistantTutorialPreference = new Preference(context);
+        launchAssistantTutorialPreference.setKey("launchAssistantTutorial");
+        launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial");
+        launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture");
+        launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> {
+            startActivity(launchSandboxIntent.putExtra("tutorial_type", "ASSISTANT"));
+            return true;
+        });
+        sandboxCategory.addPreference(launchAssistantTutorialPreference);
     }
 
     private String toName(String action) {
-        String str = action.replace("com.android.systemui.action.PLUGIN_", "");
+        String str = action.replace("com.android.systemui.action.PLUGIN_", "")
+                .replace("com.android.launcher3.action.PLUGIN_", "");
         StringBuilder b = new StringBuilder();
         for (String s : str.split("_")) {
             if (b.length() != 0) {
@@ -205,18 +273,20 @@
 
     private static class PluginPreference extends SwitchPreference {
         private final boolean mHasSettings;
-        private final PackageInfo mInfo;
         private final PreferenceDataStore mPluginEnabler;
+        private final String mPackageName;
+        private final List<ComponentName> mComponentNames;
 
-        public PluginPreference(Context prefContext, PackageInfo info,
-                PreferenceDataStore pluginEnabler) {
+        PluginPreference(Context prefContext, ApplicationInfo info,
+                PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
             super(prefContext);
             PackageManager pm = prefContext.getPackageManager();
             mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
                     .setPackage(info.packageName), 0) != null;
-            mInfo = info;
+            mPackageName = info.packageName;
+            mComponentNames = componentNames;
             mPluginEnabler = pluginEnabler;
-            setTitle(info.applicationInfo.loadLabel(pm));
+            setTitle(info.loadLabel(pm));
             setChecked(isPluginEnabled());
             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
         }
@@ -227,9 +297,7 @@
         }
 
         private boolean isPluginEnabled() {
-            for (int i = 0; i < mInfo.services.length; i++) {
-                ComponentName componentName = new ComponentName(mInfo.packageName,
-                        mInfo.services[i].name);
+            for (ComponentName componentName : mComponentNames) {
                 if (!isEnabled(componentName)) {
                     return false;
                 }
@@ -240,17 +308,14 @@
         @Override
         protected boolean persistBoolean(boolean isEnabled) {
             boolean shouldSendBroadcast = false;
-            for (int i = 0; i < mInfo.services.length; i++) {
-                ComponentName componentName = new ComponentName(mInfo.packageName,
-                        mInfo.services[i].name);
-
+            for (ComponentName componentName : mComponentNames) {
                 if (isEnabled(componentName) != isEnabled) {
                     mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled);
                     shouldSendBroadcast = true;
                 }
             }
             if (shouldSendBroadcast) {
-                final String pkg = mInfo.packageName;
+                final String pkg = mPackageName;
                 final Intent intent = new Intent(PLUGIN_CHANGED,
                         pkg != null ? Uri.fromParts("package", pkg, null) : null);
                 getContext().sendBroadcast(intent);
@@ -268,8 +333,7 @@
                     : View.GONE);
             holder.findViewById(R.id.settings).setOnClickListener(v -> {
                 ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
-                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(
-                                mInfo.packageName), 0);
+                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0);
                 if (result != null) {
                     v.getContext().startActivity(new Intent().setComponent(
                             new ComponentName(result.activityInfo.packageName,
@@ -278,7 +342,7 @@
             });
             holder.itemView.setOnLongClickListener(v -> {
                 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
-                intent.setData(Uri.fromParts("package", mInfo.packageName, null));
+                intent.setData(Uri.fromParts("package", mPackageName, null));
                 getContext().startActivity(intent);
                 return true;
             });
diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java
index f30470a..a91303a 100644
--- a/src/com/android/launcher3/settings/NotificationDotsPreference.java
+++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java
@@ -20,7 +20,6 @@
 
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.DialogFragment;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -30,13 +29,14 @@
 import android.util.AttributeSet;
 import android.view.View;
 
+import androidx.fragment.app.DialogFragment;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
 import com.android.launcher3.R;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.SecureSettingsObserver;
 
-import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-
 /**
  * A {@link Preference} for indicating notification dots status.
  * Also has utility methods for updating UI based on dots status changes.
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 8d2db78..d3213a1 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -23,22 +23,20 @@
 import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
-import android.app.Activity;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.content.ComponentName;
-import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
 import androidx.preference.Preference;
-import androidx.preference.PreferenceFragment;
-import androidx.preference.PreferenceFragment.OnPreferenceStartFragmentCallback;
-import androidx.preference.PreferenceFragment.OnPreferenceStartScreenCallback;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
+import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback;
 import androidx.preference.PreferenceGroup.PreferencePositionCallback;
 import androidx.preference.PreferenceScreen;
 import androidx.recyclerview.widget.RecyclerView;
@@ -47,14 +45,13 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.GridOptionsProvider;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.SecureSettingsObserver;
 
 /**
  * Settings activity for Launcher. Currently implements the following setting: Allow rotation
  */
-public class SettingsActivity extends Activity
+public class SettingsActivity extends FragmentActivity
         implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback,
         SharedPreferences.OnSharedPreferenceChangeListener{
 
@@ -70,8 +67,6 @@
     private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600;
     public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
 
-    public static final String GRID_OPTIONS_PREFERENCE_KEY = "pref_grid_options";
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -83,73 +78,53 @@
                 args.putString(EXTRA_FRAGMENT_ARG_KEY, prefKey);
             }
 
-            Fragment f = Fragment.instantiate(
-                    this, getString(R.string.settings_fragment_name), args);
+            final FragmentManager fm = getSupportFragmentManager();
+            final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(),
+                    getString(R.string.settings_fragment_name));
+            f.setArguments(args);
             // Display the fragment as the main content.
-            getFragmentManager().beginTransaction()
-                    .replace(android.R.id.content, f)
-                    .commit();
+            fm.beginTransaction().replace(android.R.id.content, f).commit();
         }
         Utilities.getPrefs(getApplicationContext()).registerOnSharedPreferenceChangeListener(this);
     }
+
     @Override
-    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
-        if (GRID_OPTIONS_PREFERENCE_KEY.equals(key)) {
-
-            final ComponentName cn = new ComponentName(getApplicationContext(),
-                    GridOptionsProvider.class);
-            Context c = getApplicationContext();
-            int oldValue = c.getPackageManager().getComponentEnabledSetting(cn);
-            int newValue;
-            if (Utilities.getPrefs(c).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) {
-                newValue = PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-            } else {
-                newValue = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
-            }
-
-            if (oldValue != newValue) {
-                c.getPackageManager().setComponentEnabledSetting(cn, newValue,
-                        PackageManager.DONT_KILL_APP);
-            }
-        }
-    }
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { }
 
     private boolean startFragment(String fragment, Bundle args, String key) {
-        if (Utilities.ATLEAST_P && getFragmentManager().isStateSaved()) {
+        if (Utilities.ATLEAST_P && getSupportFragmentManager().isStateSaved()) {
             // Sometimes onClick can come after onPause because of being posted on the handler.
             // Skip starting new fragments in that case.
             return false;
         }
-        Fragment f = Fragment.instantiate(this, fragment, args);
+        final FragmentManager fm = getSupportFragmentManager();
+        final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(), fragment);
+        f.setArguments(args);
         if (f instanceof DialogFragment) {
-            ((DialogFragment) f).show(getFragmentManager(), key);
+            ((DialogFragment) f).show(getSupportFragmentManager(), key);
         } else {
-            getFragmentManager()
-                    .beginTransaction()
-                    .replace(android.R.id.content, f)
-                    .addToBackStack(key)
-                    .commit();
+            fm.beginTransaction().replace(android.R.id.content, f).addToBackStack(key).commit();
         }
         return true;
     }
 
     @Override
     public boolean onPreferenceStartFragment(
-            PreferenceFragment preferenceFragment, Preference pref) {
+            PreferenceFragmentCompat preferenceFragment, Preference pref) {
         return startFragment(pref.getFragment(), pref.getExtras(), pref.getKey());
     }
 
     @Override
-    public boolean onPreferenceStartScreen(PreferenceFragment caller, PreferenceScreen pref) {
+    public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
         Bundle args = new Bundle();
-        args.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey());
+        args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.getKey());
         return startFragment(getString(R.string.settings_fragment_name), args, pref.getKey());
     }
 
     /**
      * This fragment shows the launcher preferences.
      */
-    public static class LauncherSettingsFragment extends PreferenceFragment {
+    public static class LauncherSettingsFragment extends PreferenceFragmentCompat {
 
         private SecureSettingsObserver mNotificationDotsObserver;
 
@@ -233,10 +208,6 @@
                     // Show if plugins are enabled or flag UI is enabled.
                     return FeatureFlags.showFlagTogglerUi(getContext()) ||
                             PluginManagerWrapper.hasPlugins(getContext());
-                case GRID_OPTIONS_PREFERENCE_KEY:
-                    return Utilities.isDevelopersOptionsEnabled(getContext()) &&
-                            Utilities.IS_DEBUG_DEVICE &&
-                            Utilities.existsStyleWallpapers(getContext());
             }
 
             return true;
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index 9274d44..e9b92e2 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -27,8 +27,8 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 
 /**
@@ -111,8 +111,8 @@
 
         // TODO: Add the click handler to this view directly and not the child view.
         mBubbleText.setOnClickListener(container.getItemClickListener());
-        mBubbleText.setOnLongClickListener(container);
-        mBubbleText.setOnTouchListener(container);
+        mBubbleText.setOnLongClickListener(container.getItemDragHandler());
+        mBubbleText.setOnTouchListener(container.getItemDragHandler());
     }
 
     /**
diff --git a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
index ee97641..3e59b61 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
@@ -25,7 +25,9 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.DragPreviewProvider;
+import com.android.launcher3.icons.BitmapRenderer;
 
 /**
  * Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size.
@@ -39,16 +41,27 @@
         mPositionShift = shift;
     }
 
+    @Override
     public Bitmap createDragBitmap() {
+        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
+            return BitmapRenderer.createHardwareBitmap(
+                    size + blurSizeOutline,
+                    size + blurSizeOutline,
+                    (c) -> drawDragViewOnBackground(c, size));
+        } else {
+            return createDragBitmapLegacy();
+        }
+    }
+
+    private Bitmap createDragBitmapLegacy() {
         Drawable d = mView.getBackground();
         Rect bounds = getDrawableBounds(d);
-
         int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
         final Bitmap b = Bitmap.createBitmap(
                 size + blurSizeOutline,
                 size + blurSizeOutline,
                 Bitmap.Config.ARGB_8888);
-
         Canvas canvas = new Canvas(b);
         canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
         canvas.scale(((float) size) / bounds.width(), ((float) size) / bounds.height(), 0, 0);
@@ -57,6 +70,16 @@
         return b;
     }
 
+    private void drawDragViewOnBackground(Canvas canvas, float size) {
+        Drawable d = mView.getBackground();
+        Rect bounds = getDrawableBounds(d);
+
+        canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
+        canvas.scale(size / bounds.width(), size / bounds.height(), 0, 0);
+        canvas.translate(bounds.left, bounds.top);
+        d.draw(canvas);
+    }
+
     @Override
     public float getScaleAndPosition(Bitmap preview, int[] outPos) {
         Launcher launcher = Launcher.getLauncher(mView.getContext());
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 70665ca..3ca9490 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -1,11 +1,12 @@
 package com.android.launcher3.shortcuts;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.ComponentKey;
 
 /**
@@ -29,6 +30,14 @@
         return componentName.getClassName();
     }
 
+    /**
+     * Creates a {@link ShortcutRequest} for this key
+     */
+    public ShortcutRequest buildRequest(Context context) {
+        return new ShortcutRequest(context, user)
+                .forPackage(componentName.getPackageName(), getId());
+    }
+
     public static ShortcutKey fromInfo(ShortcutInfo shortcutInfo) {
         return new ShortcutKey(shortcutInfo.getPackage(), shortcutInfo.getUserHandle(),
                 shortcutInfo.getId());
diff --git a/src/com/android/launcher3/shortcuts/ShortcutRequest.java b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
new file mode 100644
index 0000000..5291ce4
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shortcuts;
+
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ShortcutQuery;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class to streamline Shortcut query
+ */
+public class ShortcutRequest {
+
+    private static final String TAG = "ShortcutRequest";
+
+    public static final int ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
+            | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
+    public static final int PUBLISHED = ShortcutQuery.FLAG_MATCH_DYNAMIC
+            | ShortcutQuery.FLAG_MATCH_MANIFEST;
+    public static final int PINNED = ShortcutQuery.FLAG_MATCH_PINNED;
+
+    private final ShortcutQuery mQuery = GO_DISABLE_WIDGETS ? null : new ShortcutQuery();
+
+    private final Context mContext;
+    private final UserHandle mUserHandle;
+
+    boolean mFailed = false;
+
+    public ShortcutRequest(Context context, UserHandle userHandle) {
+        mContext = context;
+        mUserHandle = userHandle;
+    }
+
+    /** @see #forPackage(String, List) */
+    public ShortcutRequest forPackage(String packageName) {
+        return forPackage(packageName, (List<String>) null);
+    }
+
+    /** @see #forPackage(String, List) */
+    public ShortcutRequest forPackage(String packageName, String... shortcutIds) {
+        return forPackage(packageName, Arrays.asList(shortcutIds));
+    }
+
+    /**
+     * @param shortcutIds If null, match all shortcuts, otherwise only match the given id's.
+     * @return A list of ShortcutInfo's associated with the given package.
+     */
+    public ShortcutRequest forPackage(String packageName, @Nullable List<String> shortcutIds) {
+        if (!GO_DISABLE_WIDGETS && packageName != null) {
+            mQuery.setPackage(packageName);
+            mQuery.setShortcutIds(shortcutIds);
+        }
+        return this;
+    }
+
+    public ShortcutRequest withContainer(@Nullable ComponentName activity) {
+        if (!GO_DISABLE_WIDGETS) {
+            if (activity == null) {
+                mFailed = true;
+            } else {
+                mQuery.setActivity(activity);
+            }
+        }
+        return this;
+    }
+
+    public QueryResult query(int flags) {
+        if (GO_DISABLE_WIDGETS || mFailed) {
+            return QueryResult.DEFAULT;
+        }
+        mQuery.setQueryFlags(flags);
+
+        try {
+            return new QueryResult(mContext.getSystemService(LauncherApps.class)
+                    .getShortcuts(mQuery, mUserHandle));
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to query for shortcuts", e);
+            return QueryResult.DEFAULT;
+        }
+    }
+
+    public static class QueryResult extends ArrayList<ShortcutInfo> {
+
+        static final QueryResult DEFAULT = new QueryResult(GO_DISABLE_WIDGETS);
+
+        private final boolean mWasSuccess;
+
+        QueryResult(List<ShortcutInfo> result) {
+            super(result == null ? Collections.emptyList() : result);
+            mWasSuccess = true;
+        }
+
+        QueryResult(boolean wasSuccess) {
+            mWasSuccess = wasSuccess;
+        }
+
+
+        public boolean wasSuccess() {
+            return mWasSuccess;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
new file mode 100644
index 0000000..daec1d8
--- /dev/null
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -0,0 +1,55 @@
+/*
+ * 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.statemanager;
+
+import android.content.Context;
+
+/**
+ * Interface representing a state of a StatefulActivity
+ */
+public interface BaseState<T extends BaseState> {
+
+    // Flag to indicate that Launcher is non-interactive in this state
+    int FLAG_NON_INTERACTIVE = 1 << 0;
+    int FLAG_DISABLE_RESTORE = 1 << 1;
+
+    static int getFlag(int index) {
+        // reserve few spots to base flags
+        return 1 << (index + 2);
+    }
+
+    /**
+     * @return How long the animation to this state should take (or from this state to NORMAL).
+     */
+    int getTransitionDuration(Context context);
+
+    /**
+     * Returns the state to go back to from this state
+     */
+    T getHistoryForState(T previousState);
+
+    /**
+     * @return true if the state can be persisted across activity restarts.
+     */
+    default boolean shouldDisableRestore() {
+        return hasFlag(FLAG_DISABLE_RESTORE);
+    }
+
+    /**
+     * Returns if the state has the provided flag
+     */
+    boolean hasFlag(int flagMask);
+}
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
new file mode 100644
index 0000000..60b87d9
--- /dev/null
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -0,0 +1,597 @@
+/*
+ * 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.statemanager;
+
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.TestProtocol;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Class to manage transitions between different states for a StatefulActivity based on different
+ * states
+ */
+public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
+
+    public static final String TAG = "StateManager";
+
+    private final AnimationState mConfig = new AnimationState();
+    private final Handler mUiHandler;
+    private final StatefulActivity<STATE_TYPE> mActivity;
+    private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
+    private final STATE_TYPE mBaseState;
+
+    // Animators which are run on properties also controlled by state animations.
+    private final AtomicAnimationFactory mAtomicAnimationFactory;
+
+    private StateHandler<STATE_TYPE>[] mStateHandlers;
+    private STATE_TYPE mState;
+
+    private STATE_TYPE mLastStableState;
+    private STATE_TYPE mCurrentStableState;
+
+    private STATE_TYPE mRestState;
+
+    public StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState) {
+        mUiHandler = new Handler(Looper.getMainLooper());
+        mActivity = l;
+        mBaseState = baseState;
+        mState = mLastStableState = mCurrentStableState = baseState;
+        mAtomicAnimationFactory = l.createAtomicAnimationFactory();
+    }
+
+    public STATE_TYPE getState() {
+        return mState;
+    }
+
+    public STATE_TYPE getCurrentStableState() {
+        return mCurrentStableState;
+    }
+
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "StateManager:");
+        writer.println(prefix + "\tmLastStableState:" + mLastStableState);
+        writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
+        writer.println(prefix + "\tmState:" + mState);
+        writer.println(prefix + "\tmRestState:" + mRestState);
+        writer.println(prefix + "\tisInTransition:" + (mConfig.currentAnimation != null));
+    }
+
+    public StateHandler[] getStateHandlers() {
+        if (mStateHandlers == null) {
+            mStateHandlers = mActivity.createStateHandlers();
+        }
+        return mStateHandlers;
+    }
+
+    public void addStateListener(StateListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeStateListener(StateListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Returns true if the state changes should be animated.
+     */
+    public boolean shouldAnimateStateChange() {
+        return !mActivity.isForceInvisible() && mActivity.isStarted();
+    }
+
+    /**
+     * @return {@code true} if the state matches the current state and there is no active
+     *         transition to different state.
+     */
+    public boolean isInStableState(STATE_TYPE state) {
+        return mState == state && mCurrentStableState == state
+                && (mConfig.targetState == null || mConfig.targetState == state);
+    }
+
+    /**
+     * @see #goToState(STATE_TYPE, boolean, Runnable)
+     */
+    public void goToState(STATE_TYPE state) {
+        goToState(state, shouldAnimateStateChange());
+    }
+
+    /**
+     * @see #goToState(STATE_TYPE, boolean, Runnable)
+     */
+    public void goToState(STATE_TYPE state, boolean animated) {
+        goToState(state, animated, 0, null);
+    }
+
+    /**
+     * Changes the Launcher state to the provided state.
+     *
+     * @param animated false if the state should change immediately without any animation,
+     *                true otherwise
+     * @paras onCompleteRunnable any action to perform at the end of the transition, of null.
+     */
+    public void goToState(STATE_TYPE state, boolean animated, Runnable onCompleteRunnable) {
+        goToState(state, animated, 0, onCompleteRunnable);
+    }
+
+    /**
+     * Changes the Launcher state to the provided state after the given delay.
+     */
+    public void goToState(STATE_TYPE state, long delay, Runnable onCompleteRunnable) {
+        goToState(state, true, delay, onCompleteRunnable);
+    }
+
+    /**
+     * Changes the Launcher state to the provided state after the given delay.
+     */
+    public void goToState(STATE_TYPE state, long delay) {
+        goToState(state, true, delay, null);
+    }
+
+    public void reapplyState() {
+        reapplyState(false);
+    }
+
+    public void reapplyState(boolean cancelCurrentAnimation) {
+        boolean wasInAnimation = mConfig.currentAnimation != null;
+        if (cancelCurrentAnimation) {
+            mAtomicAnimationFactory.cancelAllStateElementAnimation();
+            cancelAnimation();
+        }
+        if (mConfig.currentAnimation == null) {
+            for (StateHandler handler : getStateHandlers()) {
+                handler.setState(mState);
+            }
+            if (wasInAnimation) {
+                onStateTransitionEnd(mState);
+            }
+        }
+    }
+
+    private void goToState(STATE_TYPE state, boolean animated, long delay,
+            final Runnable onCompleteRunnable) {
+        animated &= Utilities.areAnimationsEnabled(mActivity);
+        if (mActivity.isInState(state)) {
+            if (mConfig.currentAnimation == null) {
+                // Run any queued runnable
+                if (onCompleteRunnable != null) {
+                    onCompleteRunnable.run();
+                }
+                return;
+            } else if (!mConfig.userControlled && animated && mConfig.targetState == state) {
+                // We are running the same animation as requested
+                if (onCompleteRunnable != null) {
+                    mConfig.currentAnimation.addListener(
+                            AnimationSuccessListener.forRunnable(onCompleteRunnable));
+                }
+                return;
+            }
+        }
+
+        // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
+        STATE_TYPE fromState = mState;
+        mConfig.reset();
+
+        if (!animated) {
+            mAtomicAnimationFactory.cancelAllStateElementAnimation();
+            onStateTransitionStart(state);
+            for (StateHandler handler : getStateHandlers()) {
+                handler.setState(state);
+            }
+
+            onStateTransitionEnd(state);
+
+            // Run any queued runnable
+            if (onCompleteRunnable != null) {
+                onCompleteRunnable.run();
+            }
+            return;
+        }
+
+        if (delay > 0) {
+            // Create the animation after the delay as some properties can change between preparing
+            // the animation and running the animation.
+            int startChangeId = mConfig.changeId;
+            mUiHandler.postDelayed(() -> {
+                if (mConfig.changeId == startChangeId) {
+                    goToStateAnimated(state, fromState, onCompleteRunnable);
+                }
+            }, delay);
+        } else {
+            goToStateAnimated(state, fromState, onCompleteRunnable);
+        }
+    }
+
+    private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
+            Runnable onCompleteRunnable) {
+        // Since state mBaseState can be reached from multiple states, just assume that the
+        // transition plays in reverse and use the same duration as previous state.
+        mConfig.duration = state == mBaseState
+                ? fromState.getTransitionDuration(mActivity)
+                : state.getTransitionDuration(mActivity);
+        prepareForAtomicAnimation(fromState, state, mConfig);
+        AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
+        if (onCompleteRunnable != null) {
+            animation.addListener(AnimationSuccessListener.forRunnable(onCompleteRunnable));
+        }
+        mUiHandler.post(new StartAnimRunnable(animation));
+    }
+
+    /**
+     * Prepares for a non-user controlled animation from fromState to toState. Preparations include:
+     * - Setting interpolators for various animations included in the state transition.
+     * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
+     */
+    public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
+            StateAnimationConfig config) {
+        mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
+    }
+
+    /**
+     * Creates an animation representing atomic transitions between the provided states
+     */
+    public AnimatorSet createAtomicAnimation(
+            STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
+        PendingAnimation builder = new PendingAnimation(config.duration);
+        prepareForAtomicAnimation(fromState, toState, config);
+
+        for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
+            handler.setStateWithAnimation(toState, config, builder);
+        }
+        return builder.buildAnim();
+    }
+
+    /**
+     * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
+     * state transition.
+     * @param state the final state for the transition.
+     * @param duration intended duration for state playback. Use higher duration for better
+     *                accuracy.
+     */
+    public AnimatorPlaybackController createAnimationToNewWorkspace(
+            STATE_TYPE state, long duration) {
+        return createAnimationToNewWorkspace(state, duration, ANIM_ALL_COMPONENTS);
+    }
+
+    public AnimatorPlaybackController createAnimationToNewWorkspace(
+            STATE_TYPE state, long duration, @AnimationFlags int animComponents) {
+        StateAnimationConfig config = new StateAnimationConfig();
+        config.duration = duration;
+        config.animFlags = animComponents;
+        return createAnimationToNewWorkspace(state, config);
+    }
+
+    public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
+            StateAnimationConfig config) {
+        config.userControlled = true;
+        mConfig.reset();
+        config.copyTo(mConfig);
+        mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
+                .createPlaybackController();
+        return mConfig.playbackController;
+    }
+
+    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()) {
+                handler.setStateWithAnimation(state, mConfig, builder);
+            }
+        }
+        builder.addListener(new AnimationSuccessListener() {
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                // Change the internal state only when the transition actually starts
+                onStateTransitionStart(state);
+            }
+
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                if (TestProtocol.sDebugTracing) {
+                    Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
+                }
+                onStateTransitionEnd(state);
+            }
+        });
+        mConfig.setAnimation(builder.buildAnim(), state);
+        return builder;
+    }
+
+    private void onStateTransitionStart(STATE_TYPE state) {
+        mState = state;
+        mActivity.onStateSetStart(mState);
+
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            mListeners.get(i).onStateTransitionStart(state);
+        }
+    }
+
+    private void onStateTransitionEnd(STATE_TYPE state) {
+        // Only change the stable states after the transitions have finished
+        if (state != mCurrentStableState) {
+            mLastStableState = state.getHistoryForState(mCurrentStableState);
+            mCurrentStableState = state;
+        }
+
+        mActivity.onStateSetEnd(state);
+        if (state == mBaseState) {
+            setRestState(null);
+        }
+
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            mListeners.get(i).onStateTransitionComplete(state);
+        }
+    }
+
+    public STATE_TYPE getLastState() {
+        return mLastStableState;
+    }
+
+    public void moveToRestState() {
+        if (mConfig.currentAnimation != null && mConfig.userControlled) {
+            // The user is doing something. Lets not mess it up
+            return;
+        }
+        if (mState.shouldDisableRestore()) {
+            goToState(getRestState());
+            // Reset history
+            mLastStableState = mBaseState;
+        }
+    }
+
+    public STATE_TYPE getRestState() {
+        return mRestState == null ? mBaseState : mRestState;
+    }
+
+    public void setRestState(STATE_TYPE restState) {
+        mRestState = restState;
+    }
+
+    /**
+     * Cancels the current animation.
+     */
+    public void cancelAnimation() {
+        mConfig.reset();
+    }
+
+    public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
+        clearCurrentAnimation();
+        setCurrentAnimation(controller.getTarget());
+        mConfig.userControlled = true;
+        mConfig.playbackController = controller;
+    }
+
+    /**
+     * Sets the animation as the current state animation, i.e., canceled when
+     * starting another animation and may block some launcher interactions while running.
+     *
+     * @param childAnimations Set of animations with the new target is controlling.
+     */
+    public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
+        for (Animator childAnim : childAnimations) {
+            if (childAnim == null) {
+                continue;
+            }
+            if (mConfig.playbackController != null
+                    && mConfig.playbackController.getTarget() == childAnim) {
+                clearCurrentAnimation();
+                break;
+            } else if (mConfig.currentAnimation == childAnim) {
+                clearCurrentAnimation();
+                break;
+            }
+        }
+        boolean reapplyNeeded = mConfig.currentAnimation != null;
+        cancelAnimation();
+        if (reapplyNeeded) {
+            reapplyState();
+            // Dispatch on transition end, so that any transient property is cleared.
+            onStateTransitionEnd(mState);
+        }
+        mConfig.setAnimation(anim, null);
+    }
+
+    /**
+     * Cancels a currently running gesture animation
+     */
+    public void cancelStateElementAnimation(int index) {
+        if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) {
+            mAtomicAnimationFactory.mStateElementAnimators[index].cancel();
+        }
+    }
+
+    public Animator createStateElementAnimation(int index, float... values) {
+        cancelStateElementAnimation(index);
+        Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values);
+        mAtomicAnimationFactory.mStateElementAnimators[index] = anim;
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAtomicAnimationFactory.mStateElementAnimators[index] = null;
+            }
+        });
+        return anim;
+    }
+
+    private void clearCurrentAnimation() {
+        if (mConfig.currentAnimation != null) {
+            mConfig.currentAnimation.removeListener(mConfig);
+            mConfig.currentAnimation = null;
+        }
+        mConfig.playbackController = null;
+    }
+
+    private class StartAnimRunnable implements Runnable {
+
+        private final AnimatorSet mAnim;
+
+        public StartAnimRunnable(AnimatorSet anim) {
+            mAnim = anim;
+        }
+
+        @Override
+        public void run() {
+            if (mConfig.currentAnimation != mAnim) {
+                return;
+            }
+            mAnim.start();
+        }
+    }
+
+    private static class AnimationState<STATE_TYPE> extends StateAnimationConfig
+            implements AnimatorListener {
+
+        private static final StateAnimationConfig DEFAULT = new StateAnimationConfig();
+
+        public AnimatorPlaybackController playbackController;
+        public AnimatorSet currentAnimation;
+        public STATE_TYPE targetState;
+
+        // Id to keep track of config changes, to tie an animation with the corresponding request
+        public int changeId = 0;
+
+        /**
+         * Cancels the current animation and resets config variables.
+         */
+        public void reset() {
+            DEFAULT.copyTo(this);
+            targetState = null;
+
+            if (playbackController != null) {
+                playbackController.getAnimationPlayer().cancel();
+                playbackController.dispatchOnCancel();
+            } else if (currentAnimation != null) {
+                currentAnimation.setDuration(0);
+                currentAnimation.cancel();
+            }
+
+            currentAnimation = null;
+            playbackController = null;
+            changeId++;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (playbackController != null && playbackController.getTarget() == animation) {
+                playbackController = null;
+            }
+            if (currentAnimation == animation) {
+                currentAnimation = null;
+            }
+        }
+
+        public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) {
+            currentAnimation = animation;
+            this.targetState = targetState;
+            currentAnimation.addListener(this);
+        }
+
+        @Override
+        public void onAnimationStart(Animator animator) { }
+
+        @Override
+        public void onAnimationCancel(Animator animator) { }
+
+        @Override
+        public void onAnimationRepeat(Animator animator) { }
+    }
+
+    public interface StateHandler<STATE_TYPE> {
+
+        /**
+         * Updates the UI to {@param state} without any animations
+         */
+        void setState(STATE_TYPE state);
+
+        /**
+         * Sets the UI to {@param state} by animating any changes.
+         */
+        void setStateWithAnimation(
+                STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation);
+    }
+
+    public interface StateListener<STATE_TYPE> {
+
+        default void onStateTransitionStart(STATE_TYPE toState) { }
+
+        default void onStateTransitionComplete(STATE_TYPE finalState) { }
+    }
+
+    /**
+     * Factory class to configure and create atomic animations.
+     */
+    public static class AtomicAnimationFactory<STATE_TYPE> {
+
+        protected static final int NEXT_INDEX = 0;
+
+        private final Animator[] mStateElementAnimators;
+
+        /**
+         *
+         * @param sharedElementAnimCount number of animations which run on state properties
+         */
+        public AtomicAnimationFactory(int sharedElementAnimCount) {
+            mStateElementAnimators = new Animator[sharedElementAnimCount];
+        }
+
+        void cancelAllStateElementAnimation() {
+            for (Animator animator : mStateElementAnimators) {
+                if (animator != null) {
+                    animator.cancel();
+                }
+            }
+        }
+
+        /**
+         * Creates animations for elements which can be also be part of state transitions. The
+         * actual definition of the animation is up to the app to define.
+         *
+         */
+        public Animator createStateElementAnimation(int index, float... values) {
+            throw new RuntimeException("Unknown gesture animation " + index);
+        }
+
+        /**
+         * Prepares for a non-user controlled animation from fromState to this state. Preparations
+         * include:
+         * - Setting interpolators for various animations included in the state transition.
+         * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
+         */
+        public void prepareForAtomicAnimation(
+                STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { }
+    }
+}
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
new file mode 100644
index 0000000..dbe5f42
--- /dev/null
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.statemanager;
+
+import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
+
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.CallSuper;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.LauncherRootView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.views.BaseDragLayer;
+
+/**
+ * Abstract activity with state management
+ * @param <STATE_TYPE> Type of state object
+ */
+public abstract class StatefulActivity<STATE_TYPE extends BaseState<STATE_TYPE>>
+        extends BaseDraggingActivity {
+
+    public final Handler mHandler = new Handler();
+    private final Runnable mHandleDeferredResume = this::handleDeferredResume;
+    private boolean mDeferredResumePending;
+
+    private LauncherRootView mRootView;
+
+    /**
+     * Create handlers to control the property changes for this activity
+     */
+    protected abstract StateHandler<STATE_TYPE>[] createStateHandlers();
+
+    /**
+     * Returns true if the activity is in the provided state
+     */
+    public boolean isInState(STATE_TYPE state) {
+        return getStateManager().getState() == state;
+    }
+
+    /**
+     * Returns the state manager for this activity
+     */
+    public abstract StateManager<STATE_TYPE> getStateManager();
+
+    protected void inflateRootView(int layoutId) {
+        mRootView = (LauncherRootView) LayoutInflater.from(this).inflate(layoutId, null);
+        mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+    }
+
+    @Override
+    public final LauncherRootView getRootView() {
+        return mRootView;
+    }
+
+    @Override
+    public <T extends View> T findViewById(int id) {
+        return mRootView.findViewById(id);
+    }
+
+    /**
+     * Called when transition to the state starts
+     */
+    @CallSuper
+    public void onStateSetStart(STATE_TYPE state) {
+        if (mDeferredResumePending) {
+            handleDeferredResume();
+        }
+    }
+
+    /**
+     * Called when transition to state ends
+     */
+    public void onStateSetEnd(STATE_TYPE state) { }
+
+    /**
+     * Creates a factory for atomic state animations
+     */
+    public AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
+        return new AtomicAnimationFactory(0);
+    }
+
+    @Override
+    public void reapplyUi() {
+        reapplyUi(true /* cancelCurrentAnimation */);
+    }
+
+    /**
+     * Re-applies if any state transition is not running, optionally cancelling
+     * the transition if requested.
+     */
+    public void reapplyUi(boolean cancelCurrentAnimation) {
+        getRootView().dispatchInsets();
+        getStateManager().reapplyState(cancelCurrentAnimation);
+    }
+
+    @Override
+    protected void onStop() {
+        BaseDragLayer dragLayer = getDragLayer();
+        final boolean wasActive = isUserActive();
+        final STATE_TYPE origState = getStateManager().getState();
+        final int origDragLayerChildCount = dragLayer.getChildCount();
+        super.onStop();
+
+        getStateManager().moveToRestState();
+
+        // Workaround for b/78520668, explicitly trim memory once UI is hidden
+        onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
+
+        if (wasActive) {
+            // The expected condition is that this activity is stopped because the device goes to
+            // sleep and the UI may have noticeable changes.
+            dragLayer.post(() -> {
+                if ((!getStateManager().isInStableState(origState)
+                        // The drag layer may be animating (e.g. dismissing QSB).
+                        || dragLayer.getAlpha() < 1
+                        // Maybe an ArrowPopup is closed.
+                        || dragLayer.getChildCount() != origDragLayerChildCount)) {
+                    onUiChangedWhileSleeping();
+                }
+            });
+        }
+    }
+
+    /**
+     * Called if the Activity UI changed while the activity was not visible
+     */
+    protected void onUiChangedWhileSleeping() { }
+
+    private void handleDeferredResume() {
+        if (hasBeenResumed() && !getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)) {
+            onDeferredResumed();
+            addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
+
+            mDeferredResumePending = false;
+        } else {
+            mDeferredResumePending = true;
+        }
+    }
+
+    /**
+     * Called want the activity has stayed resumed for 1 frame.
+     */
+    protected void onDeferredResumed() { }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        mHandler.removeCallbacks(mHandleDeferredResume);
+        Utilities.postAsyncCallback(mHandler, mHandleDeferredResume);
+    }
+}
diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java
new file mode 100644
index 0000000..b8a184f
--- /dev/null
+++ b/src/com/android/launcher3/states/HintState.java
@@ -0,0 +1,61 @@
+/*
+ * 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.states;
+
+import android.content.Context;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+
+/**
+ * Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen.
+ */
+public class HintState extends LauncherState {
+
+    private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE | FLAG_DISABLE_RESTORE
+            | FLAG_HAS_SYS_UI_SCRIM;
+
+    public HintState(int id) {
+        super(id, ContainerType.DEFAULT_CONTAINERTYPE, STATE_FLAGS);
+    }
+
+    @Override
+    public int getTransitionDuration(Context context) {
+        return 80;
+    }
+
+    @Override
+    protected float getDepthUnchecked(Context context) {
+        return 0.15f;
+    }
+
+    @Override
+    public float getOverviewScrimAlpha(Launcher launcher) {
+        return 0.4f;
+    }
+
+    @Override
+    public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
+        return new ScaleAndTranslation(0.92f, 0, 0);
+    }
+
+    @Override
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        // Treat the QSB as part of the hotseat so they move together.
+        return getHotseatScaleAndTranslation(launcher);
+    }
+}
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
deleted file mode 100644
index a23cd6d..0000000
--- a/src/com/android/launcher3/states/InternalStateHandler.java
+++ /dev/null
@@ -1,145 +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.states;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Intent;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.IBinder;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Utility class to sending state handling logic to Launcher from within the same process.
- *
- * Extending {@link Binder} ensures that the platform maintains a single instance of each object
- * which allows this object to safely navigate the system process.
- */
-public abstract class InternalStateHandler extends Binder {
-
-    public static final String EXTRA_STATE_HANDLER = "launcher.state_handler";
-
-    private static final Scheduler sScheduler = new Scheduler();
-
-    /**
-     * Initializes the handler when the launcher is ready.
-     * @return true if the handler wants to stay alive.
-     */
-    protected abstract boolean init(Launcher launcher, boolean alreadyOnHome);
-
-    public final Intent addToIntent(Intent intent) {
-        Bundle extras = new Bundle();
-        extras.putBinder(EXTRA_STATE_HANDLER, this);
-        intent.putExtras(extras);
-        return intent;
-    }
-
-    public final void initWhenReady() {
-        sScheduler.schedule(this);
-    }
-
-    public boolean clearReference() {
-        return sScheduler.clearReference(this);
-    }
-
-    public static boolean hasPending() {
-        return sScheduler.hasPending();
-    }
-
-    public static boolean handleCreate(Launcher launcher, Intent intent) {
-        return handleIntent(launcher, intent, false, false);
-    }
-
-    public static boolean handleNewIntent(Launcher launcher, Intent intent, boolean alreadyOnHome) {
-        return handleIntent(launcher, intent, alreadyOnHome, true);
-    }
-
-    private static boolean handleIntent(
-            Launcher launcher, Intent intent, boolean alreadyOnHome, boolean explicitIntent) {
-        boolean result = false;
-        if (intent != null && intent.getExtras() != null) {
-            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
-            if (stateBinder instanceof InternalStateHandler) {
-                InternalStateHandler handler = (InternalStateHandler) stateBinder;
-                if (!handler.init(launcher, alreadyOnHome)) {
-                    intent.getExtras().remove(EXTRA_STATE_HANDLER);
-                }
-                result = true;
-            }
-        }
-        if (!result && !explicitIntent) {
-            result = sScheduler.initIfPending(launcher, alreadyOnHome);
-        }
-        return result;
-    }
-
-    private static class Scheduler implements Runnable {
-
-        private WeakReference<InternalStateHandler> mPendingHandler = new WeakReference<>(null);
-
-        public void schedule(InternalStateHandler handler) {
-            synchronized (this) {
-                mPendingHandler = new WeakReference<>(handler);
-            }
-            MAIN_EXECUTOR.execute(this);
-        }
-
-        @Override
-        public void run() {
-            LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-            if (app == null) {
-                return;
-            }
-            Callbacks cb = app.getModel().getCallback();
-            if (!(cb instanceof Launcher)) {
-                return;
-            }
-            Launcher launcher = (Launcher) cb;
-            initIfPending(launcher, launcher.isStarted());
-        }
-
-        public boolean initIfPending(Launcher launcher, boolean alreadyOnHome) {
-            InternalStateHandler pendingHandler = mPendingHandler.get();
-            if (pendingHandler != null) {
-                if (!pendingHandler.init(launcher, alreadyOnHome)) {
-                    clearReference(pendingHandler);
-                }
-                return true;
-            }
-            return false;
-        }
-
-        public boolean clearReference(InternalStateHandler handler) {
-            synchronized (this) {
-                if (mPendingHandler.get() == handler) {
-                    mPendingHandler.clear();
-                    return true;
-                }
-                return false;
-            }
-        }
-
-        public boolean hasPending() {
-            return mPendingHandler.get() != null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index abf90e2..5a60f53 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -20,16 +20,18 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 
+import android.app.Activity;
+import android.content.ContentResolver;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.res.Resources;
-import android.view.WindowManager;
-import android.view.WindowManager.LayoutParams;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Log;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.UiThreadHelper;
 
 /**
@@ -37,11 +39,23 @@
  */
 public class RotationHelper implements OnSharedPreferenceChangeListener {
 
+    private static final String TAG = "RotationHelper";
+
     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
 
+    private final ContentResolver mContentResolver;
+    private boolean mSystemAutoRotateEnabled;
+
+    private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateAutoRotateSetting();
+        }
+    };
+
     public static boolean getAllowRotationDefaultValue() {
-        // If the device was scaled, used the original dimensions to determine if rotation
-        // is allowed of not.
+        // If the device's pixel density was scaled (usually via settings for A11y), use the
+        // original dimensions to determine if rotation is allowed of not.
         Resources res = Resources.getSystem();
         int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
                 * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
@@ -52,14 +66,16 @@
     public static final int REQUEST_ROTATE = 1;
     public static final int REQUEST_LOCK = 2;
 
-    private final Launcher mLauncher;
-    private final SharedPreferences mPrefs;
+    private final Activity mActivity;
+    private final SharedPreferences mSharedPrefs;
 
     private boolean mIgnoreAutoRotateSettings;
-    private boolean mAutoRotateEnabled;
+    private boolean mHomeRotationEnabled;
 
     /**
-     * Rotation request made by {@link InternalStateHandler}. This supersedes any other request.
+     * Rotation request made by
+     * {@link com.android.launcher3.util.ActivityTracker.SchedulerCallback}.
+     * This supersedes any other request.
      */
     private int mStateHandlerRequest = REQUEST_NONE;
     /**
@@ -74,65 +90,52 @@
     // This is used to defer setting rotation flags until the activity is being created
     private boolean mInitialized;
     private boolean mDestroyed;
-    private boolean mRotationHasDifferentUI;
 
     private int mLastActivityFlags = -1;
 
-    public RotationHelper(Launcher launcher) {
-        mLauncher = launcher;
+    public RotationHelper(Activity activity) {
+        mActivity = activity;
 
         // On large devices we do not handle auto-rotate differently.
-        mIgnoreAutoRotateSettings = mLauncher.getResources().getBoolean(R.bool.allow_rotation);
+        mIgnoreAutoRotateSettings = mActivity.getResources().getBoolean(R.bool.allow_rotation);
         if (!mIgnoreAutoRotateSettings) {
-            mPrefs = Utilities.getPrefs(mLauncher);
-            mPrefs.registerOnSharedPreferenceChangeListener(this);
-            mAutoRotateEnabled = mPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+            mSharedPrefs = Utilities.getPrefs(mActivity);
+            mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
+            mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                     getAllowRotationDefaultValue());
         } else {
-            mPrefs = null;
+            mSharedPrefs = null;
         }
+
+        mContentResolver = activity.getContentResolver();
     }
 
-    public void setRotationHadDifferentUI(boolean rotationHasDifferentUI) {
-        mRotationHasDifferentUI = rotationHasDifferentUI;
-    }
-
-    public boolean homeScreenCanRotate() {
-        return mRotationHasDifferentUI || mIgnoreAutoRotateSettings || mAutoRotateEnabled
-                || mStateHandlerRequest != REQUEST_NONE
-                || mLauncher.getDeviceProfile().isMultiWindowMode;
-    }
-
-    public void updateRotationAnimation() {
-        if (FeatureFlags.FAKE_LANDSCAPE_UI.get()) {
-            WindowManager.LayoutParams lp = mLauncher.getWindow().getAttributes();
-            int oldAnim = lp.rotationAnimation;
-            lp.rotationAnimation = homeScreenCanRotate()
-                    ? WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE
-                    : WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
-            if (oldAnim != lp.rotationAnimation) {
-                mLauncher.getWindow().setAttributes(lp);
-            }
+    private void updateAutoRotateSetting() {
+        int autoRotateEnabled = 0;
+        try {
+            autoRotateEnabled = Settings.System.getInt(mContentResolver,
+                    Settings.System.ACCELEROMETER_ROTATION);
+        } catch (Settings.SettingNotFoundException e) {
+            Log.e(TAG, "autorotate setting not found", e);
         }
+
+        mSystemAutoRotateEnabled = autoRotateEnabled == 1;
     }
 
     @Override
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
-        boolean wasRotationEnabled = mAutoRotateEnabled;
-        mAutoRotateEnabled = mPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+        boolean wasRotationEnabled = mHomeRotationEnabled;
+        mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                 getAllowRotationDefaultValue());
-        if (mAutoRotateEnabled != wasRotationEnabled) {
-
+        if (mHomeRotationEnabled != wasRotationEnabled) {
             notifyChange();
-            updateRotationAnimation();
-            mLauncher.reapplyUi();
+            updateAutoRotateSetting();
         }
     }
 
     public void setStateHandlerRequest(int request) {
         if (mStateHandlerRequest != request) {
             mStateHandlerRequest = request;
-            updateRotationAnimation();
             notifyChange();
         }
     }
@@ -154,7 +157,7 @@
     // Used by tests only.
     public void forceAllowRotationForTesting(boolean allowRotation) {
         mIgnoreAutoRotateSettings =
-                allowRotation || mLauncher.getResources().getBoolean(R.bool.allow_rotation);
+                allowRotation || mActivity.getResources().getBoolean(R.bool.allow_rotation);
         notifyChange();
     }
 
@@ -162,16 +165,21 @@
         if (!mInitialized) {
             mInitialized = true;
             notifyChange();
-            updateRotationAnimation();
+
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
+                    false, mSystemAutoRotateObserver);
+            updateAutoRotateSetting();
         }
     }
 
     public void destroy() {
         if (!mDestroyed) {
             mDestroyed = true;
-            if (mPrefs != null) {
-                mPrefs.unregisterOnSharedPreferenceChangeListener(this);
+            if (mSharedPrefs != null) {
+                mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
             }
+            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
         }
     }
 
@@ -190,7 +198,7 @@
         } else if (mCurrentStateRequest == REQUEST_LOCK) {
             activityFlags = SCREEN_ORIENTATION_LOCKED;
         } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
-                || mAutoRotateEnabled) {
+                || mHomeRotationEnabled) {
             activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
         } else {
             // If auto rotation is off, allow rotation on the activity, in case the user is using
@@ -199,15 +207,27 @@
         }
         if (activityFlags != mLastActivityFlags) {
             mLastActivityFlags = activityFlags;
-            UiThreadHelper.setOrientationAsync(mLauncher, activityFlags);
+            UiThreadHelper.setOrientationAsync(mActivity, activityFlags);
         }
     }
 
+    /**
+     * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
+     * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
+     * A value of 0 means no rotation has been applied
+     */
+    public static int deltaRotation(int oldRotation, int newRotation) {
+        int delta = newRotation - oldRotation;
+        if (delta < 0) delta += 4;
+        return delta;
+    }
+
     @Override
     public String toString() {
         return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d,"
-                + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mAutoRotateEnabled=%b]",
+                + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mHomeRotationEnabled=%b,"
+                        + " mSystemAutoRotateEnabled=%b]",
                 mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags,
-                mIgnoreAutoRotateSettings, mAutoRotateEnabled);
+                mIgnoreAutoRotateSettings, mHomeRotationEnabled, mSystemAutoRotateEnabled);
     }
 }
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index be3e6c9..2a4f887 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -15,13 +15,10 @@
  */
 package com.android.launcher3.states;
 
-import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_TRANSITION_MS;
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
-
+import android.content.Context;
 import android.graphics.Rect;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
@@ -32,12 +29,18 @@
  */
 public class SpringLoadedState extends LauncherState {
 
-    private static final int STATE_FLAGS = FLAG_MULTI_PAGE |
-            FLAG_DISABLE_ACCESSIBILITY | FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED |
-            FLAG_DISABLE_PAGE_CLIPPING | FLAG_PAGE_BACKGROUNDS | FLAG_HIDE_BACK_BUTTON;
+    private static final int STATE_FLAGS = FLAG_MULTI_PAGE
+            | FLAG_WORKSPACE_INACCESSIBLE | FLAG_DISABLE_RESTORE
+            | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_WORKSPACE_HAS_BACKGROUNDS
+            | FLAG_HIDE_BACK_BUTTON;
 
     public SpringLoadedState(int id) {
-        super(id, ContainerType.OVERVIEW, SPRING_LOADED_TRANSITION_MS, STATE_FLAGS);
+        super(id, ContainerType.WORKSPACE, STATE_FLAGS);
+    }
+
+    @Override
+    public int getTransitionDuration(Context context) {
+        return 150;
     }
 
     @Override
@@ -73,33 +76,17 @@
     }
 
     @Override
+    protected float getDepthUnchecked(Context context) {
+        return 0.5f;
+    }
+
+    @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
         return new ScaleAndTranslation(1, 0, 0);
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        Workspace ws = launcher.getWorkspace();
-        ws.showPageIndicatorAtCurrentScroll();
-        ws.getPageIndicator().setShouldAutoHide(false);
-
-        // Prevent any Un/InstallShortcutReceivers from updating the db while we are
-        // in spring loaded mode
-        InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_DRAG_AND_DROP);
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_LOCK);
-    }
-
-    @Override
     public float getWorkspaceScrimAlpha(Launcher launcher) {
         return 0.3f;
     }
-
-    @Override
-    public void onStateDisabled(final Launcher launcher) {
-        launcher.getWorkspace().getPageIndicator().setShouldAutoHide(true);
-
-        // Re-enable any Un/InstallShortcutReceiver and now process any queued items
-        InstallShortcutReceiver.disableAndFlushInstallQueue(
-                InstallShortcutReceiver.FLAG_DRAG_AND_DROP, launcher);
-    }
 }
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
new file mode 100644
index 0000000..f90ad3c
--- /dev/null
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -0,0 +1,161 @@
+/*
+ * 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.states;
+
+import android.view.animation.Interpolator;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Utility class for building animator set
+ */
+public class StateAnimationConfig {
+
+    // We separate the state animations into "atomic" and "non-atomic" components. The atomic
+    // components may be run atomically - that is, all at once, instead of user-controlled. However,
+    // atomic components are not restricted to this purpose; they can be user-controlled alongside
+    // non atomic components as well. Note that each gesture model has exactly one atomic component,
+    // PLAY_ATOMIC_OVERVIEW_SCALE *or* PLAY_ATOMIC_OVERVIEW_PEEK.
+    @IntDef(flag = true, value = {
+            PLAY_NON_ATOMIC,
+            PLAY_ATOMIC_OVERVIEW_SCALE,
+            PLAY_ATOMIC_OVERVIEW_PEEK,
+            SKIP_OVERVIEW,
+            SKIP_DEPTH_CONTROLLER
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimationFlags {}
+    public static final int PLAY_NON_ATOMIC = 1 << 0;
+    public static final int PLAY_ATOMIC_OVERVIEW_SCALE = 1 << 1;
+    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 long duration;
+    public boolean userControlled;
+    public @AnimationFlags int animFlags = ANIM_ALL_COMPONENTS;
+
+    public static final int ANIM_ALL_COMPONENTS = PLAY_NON_ATOMIC | PLAY_ATOMIC_OVERVIEW_SCALE
+            | PLAY_ATOMIC_OVERVIEW_PEEK;
+
+    // Various types of animation state transition
+    @IntDef(value = {
+            ANIM_VERTICAL_PROGRESS,
+            ANIM_WORKSPACE_SCALE,
+            ANIM_WORKSPACE_TRANSLATE,
+            ANIM_WORKSPACE_FADE,
+            ANIM_HOTSEAT_SCALE,
+            ANIM_HOTSEAT_TRANSLATE,
+            ANIM_OVERVIEW_SCALE,
+            ANIM_OVERVIEW_TRANSLATE_X,
+            ANIM_OVERVIEW_TRANSLATE_Y,
+            ANIM_OVERVIEW_FADE,
+            ANIM_ALL_APPS_FADE,
+            ANIM_OVERVIEW_SCRIM_FADE,
+            ANIM_ALL_APPS_HEADER_FADE,
+            ANIM_OVERVIEW_MODAL,
+            ANIM_DEPTH,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimType {}
+    public static final int ANIM_VERTICAL_PROGRESS = 0;
+    public static final int ANIM_WORKSPACE_SCALE = 1;
+    public static final int ANIM_WORKSPACE_TRANSLATE = 2;
+    public static final int ANIM_WORKSPACE_FADE = 3;
+    public static final int ANIM_HOTSEAT_SCALE = 4;
+    public static final int ANIM_HOTSEAT_TRANSLATE = 5;
+    public static final int ANIM_OVERVIEW_SCALE = 6;
+    public static final int ANIM_OVERVIEW_TRANSLATE_X = 7;
+    public static final int ANIM_OVERVIEW_TRANSLATE_Y = 8;
+    public static final int ANIM_OVERVIEW_FADE = 9;
+    public static final int ANIM_ALL_APPS_FADE = 10;
+    public static final int ANIM_OVERVIEW_SCRIM_FADE = 11;
+    public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
+    public static final int ANIM_OVERVIEW_MODAL = 13;
+    public static final int ANIM_DEPTH = 14;
+
+    private static final int ANIM_TYPES_COUNT = 15;
+
+    private final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
+
+    public StateAnimationConfig() { }
+
+    /**
+     * Copies the config to target
+     */
+    public void copyTo(StateAnimationConfig target) {
+        target.duration = duration;
+        target.animFlags = animFlags;
+        target.userControlled = userControlled;
+        for (int i = 0; i < ANIM_TYPES_COUNT; i++) {
+            target.mInterpolators[i] = mInterpolators[i];
+        }
+    }
+
+    /**
+     * Returns the interpolator set for animId or fallback if nothing is set
+     *
+     * @see #setInterpolator(int, Interpolator)
+     */
+    public Interpolator getInterpolator(@AnimType int animId, Interpolator fallback) {
+        return mInterpolators[animId] == null ? fallback : mInterpolators[animId];
+    }
+
+    /**
+     * Sets an interpolator for a given animation type
+     */
+    public void setInterpolator(@AnimType int animId, Interpolator interpolator) {
+        mInterpolators[animId] = interpolator;
+    }
+
+    /**
+     * @return Whether Overview is scaling as part of this animation. If this is the only
+     * component (i.e. NON_ATOMIC_COMPONENT isn't included), then this scaling is happening
+     * atomically, rather than being part of a normal state animation. StateHandlers can use
+     * this to designate part of their animation that should scale with Overview.
+     */
+    public boolean playAtomicOverviewScaleComponent() {
+        return hasAnimationFlag(StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE);
+    }
+
+    /**
+     * @return Whether this animation will play atomically at the same time as a different,
+     * user-controlled state transition. StateHandlers, which contribute to both animations, can
+     * use this to avoid animating the same properties in both animations, since they'd conflict
+     * with one another.
+     */
+    public boolean onlyPlayAtomicComponent() {
+        return getAnimComponents() == StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE
+                || getAnimComponents() == StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
+    }
+
+    /**
+     * Returns true if the config and any of the provided component flags
+     */
+    public boolean hasAnimationFlag(@AnimationFlags int a) {
+        return (animFlags & a) != 0;
+    }
+
+    /**
+     * @return Only the flags that determine which animation components to play.
+     */
+    public @AnimationFlags int getAnimComponents() {
+        return animFlags & StateAnimationConfig.ANIM_ALL_COMPONENTS;
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index c6de9ca..38b3712 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -15,29 +15,33 @@
  */
 package com.android.launcher3.testing;
 
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
+import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.annotation.TargetApi;
+import android.app.Activity;
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.graphics.Insets;
+import android.os.Build;
 import android.os.Bundle;
-import android.os.Debug;
+import android.view.WindowInsets;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.util.ResourceBasedOverride;
 
-import java.util.LinkedList;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
+/**
+ * Class to handle requests from tests
+ */
+@TargetApi(Build.VERSION_CODES.Q)
 public class TestInformationHandler implements ResourceBasedOverride {
 
     public static TestInformationHandler newInstance(Context context) {
@@ -48,122 +52,117 @@
     protected Context mContext;
     protected DeviceProfile mDeviceProfile;
     protected LauncherAppState mLauncherAppState;
-    protected Launcher mLauncher;
-    private static LinkedList mLeaks;
 
     public void init(Context context) {
         mContext = context;
         mDeviceProfile = InvariantDeviceProfile.INSTANCE.
                 get(context).getDeviceProfile(context);
         mLauncherAppState = LauncherAppState.getInstanceNoCreate();
-        mLauncher = mLauncherAppState != null ?
-                (Launcher) mLauncherAppState.getModel().getCallback() : null;
     }
 
     public Bundle call(String method) {
         final Bundle response = new Bundle();
         switch (method) {
-            case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
-                if (mLauncher == null) return null;
-
-                final float progress = LauncherState.OVERVIEW.getVerticalProgress(mLauncher)
-                        - LauncherState.ALL_APPS.getVerticalProgress(mLauncher);
-                final float distance = mLauncher.getAllAppsController().getShiftRange() * progress;
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
-                break;
-            }
-
             case TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT: {
-                if (mLauncher == null) return null;
-
-                final float progress = LauncherState.NORMAL.getVerticalProgress(mLauncher)
-                        - LauncherState.ALL_APPS.getVerticalProgress(mLauncher);
-                final float distance = mLauncher.getAllAppsController().getShiftRange() * progress;
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
-                break;
+                return getLauncherUIProperty(Bundle::putInt, l -> {
+                    final float progress = LauncherState.NORMAL.getVerticalProgress(l)
+                            - LauncherState.ALL_APPS.getVerticalProgress(l);
+                    final float distance = l.getAllAppsController().getShiftRange() * progress;
+                    return (int) distance;
+                });
             }
 
             case TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED: {
-                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, isLauncherInitialized());
-                break;
+                return getUIProperty(Bundle::putBoolean, t -> isLauncherInitialized(), () -> true);
             }
 
-            case TestProtocol.REQUEST_ENABLE_DEBUG_TRACING:
-                TestProtocol.sDebugTracing = true;
-                break;
-
-            case TestProtocol.REQUEST_DISABLE_DEBUG_TRACING:
-                TestProtocol.sDebugTracing = false;
-                break;
-
             case TestProtocol.REQUEST_FREEZE_APP_LIST:
-                MAIN_EXECUTOR.execute(() ->
-                        mLauncher.getAppsView().getAppsStore().enableDeferUpdates(
-                                AllAppsStore.DEFER_UPDATES_TEST));
-                break;
-
+                return getLauncherUIProperty(Bundle::putBoolean, l -> {
+                    l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
+                    return true;
+                });
             case TestProtocol.REQUEST_UNFREEZE_APP_LIST:
-                MAIN_EXECUTOR.execute(() ->
-                        mLauncher.getAppsView().getAppsStore().disableDeferUpdates(
-                                AllAppsStore.DEFER_UPDATES_TEST));
-                break;
+                return getLauncherUIProperty(Bundle::putBoolean, l -> {
+                    l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+                    return true;
+                });
 
-            case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
-                try {
-                    final int deferUpdatesFlags = MAIN_EXECUTOR.submit(() ->
-                            mLauncher.getAppsView().getAppsStore().getDeferUpdatesFlags()).get();
-                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                            deferUpdatesFlags);
-                } catch (ExecutionException e) {
-                    throw new RuntimeException(e);
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-                break;
+            case TestProtocol.REQUEST_APPS_LIST_SCROLL_Y: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> l.getAppsView().getActiveRecyclerView().getCurrentScrollY());
             }
 
-            case TestProtocol.REQUEST_TOTAL_PSS_KB: {
-                Debug.MemoryInfo mem = new Debug.MemoryInfo();
-                Debug.getMemoryInfo(mem);
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mem.getTotalPss());
-                break;
-            }
-
-            case TestProtocol.REQUEST_JAVA_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-
-                // Allocate and dirty the memory.
-                final int leakSize = 1024 * 1024;
-                final byte[] bytes = new byte[leakSize];
-                for (int i = 0; i < leakSize; i += 239) {
-                    bytes[i] = (byte) (i % 256);
-                }
-                mLeaks.add(bytes);
-                break;
-            }
-
-            case TestProtocol.REQUEST_NATIVE_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-
-                // Allocate and dirty a bitmap.
-                final Bitmap bitmap = Bitmap.createBitmap(512, 512, ARGB_8888);
-                bitmap.eraseColor(Color.RED);
-                mLeaks.add(bitmap);
-                break;
+            case TestProtocol.REQUEST_WINDOW_INSETS: {
+                return getUIProperty(Bundle::putParcelable, a -> {
+                    WindowInsets insets = a.getWindow()
+                            .getDecorView().getRootWindowInsets();
+                    return Insets.max(
+                            insets.getSystemGestureInsets(), insets.getSystemWindowInsets());
+                }, this::getCurrentActivity);
             }
 
             case TestProtocol.REQUEST_ICON_HEIGHT: {
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                         mDeviceProfile.allAppsCellHeightPx);
-                break;
+                return response;
             }
+
+            case TestProtocol.REQUEST_MOCK_SENSOR_ROTATION:
+                TestProtocol.sDisableSensorRotation = true;
+                return response;
+
+            default:
+                return null;
         }
-        return response;
     }
 
     protected boolean isLauncherInitialized() {
-        final LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
-        return model.getCallback() == null || model.isModelLoaded();
+        return Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null
+                || LauncherAppState.getInstance(mContext).getModel().isModelLoaded();
+    }
+
+    protected Activity getCurrentActivity() {
+        return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+    }
+
+    /**
+     * Returns the result by getting a Launcher property on UI thread
+     */
+    public static <T> Bundle getLauncherUIProperty(
+            BundleSetter<T> bundleSetter, Function<Launcher, T> provider) {
+        return getUIProperty(bundleSetter, provider, Launcher.ACTIVITY_TRACKER::getCreatedActivity);
+    }
+
+    /**
+     * Returns the result by getting a generic property on UI thread
+     */
+    private static <S, T> Bundle getUIProperty(
+            BundleSetter<T> bundleSetter, Function<S, T> provider, Supplier<S> targetSupplier) {
+        try {
+            return MAIN_EXECUTOR.submit(() -> {
+                S target = targetSupplier.get();
+                if (target == null) {
+                    return null;
+                }
+                T value = provider.apply(target);
+                Bundle response = new Bundle();
+                bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
+                return response;
+            }).get();
+        } catch (ExecutionException | InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Generic interface for setting a fiend in bundle
+     * @param <T> the type of value being set
+     */
+    public interface BundleSetter<T> {
+
+        /**
+         * Sets any generic property to the bundle
+         */
+        void set(Bundle b, String key, T value);
     }
 }
-
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..51e0819
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.testing;
+
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Utilities;
+
+import java.util.function.BiConsumer;
+
+public final class TestLogging {
+    private static BiConsumer<String, String> sEventConsumer;
+
+    private static void recordEventSlow(String sequence, String event) {
+        Log.d(TestProtocol.TAPL_EVENTS_TAG, sequence + " / " + event);
+        final BiConsumer<String, String> eventConsumer = sEventConsumer;
+        if (eventConsumer != null) {
+            eventConsumer.accept(sequence, event);
+        }
+    }
+
+    public static void recordEvent(String sequence, String event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            recordEventSlow(sequence, event);
+        }
+    }
+
+    public static void recordEvent(String sequence, String message, Object parameter) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            recordEventSlow(sequence, message + ": " + parameter);
+        }
+    }
+
+    public static void recordMotionEvent(String sequence, String message, MotionEvent event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS && event.getAction() != MotionEvent.ACTION_MOVE) {
+            recordEventSlow(sequence, message + ": " + event);
+        }
+    }
+
+    static void setEventConsumer(BiConsumer<String, String> consumer) {
+        sEventConsumer = consumer;
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 07ddbdc..3ca08fd 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -20,20 +20,23 @@
  * Protocol for custom accessibility events for communication with UI Automation tests.
  */
 public final class TestProtocol {
-    public static final String GET_SCROLL_MESSAGE = "TAPL_GET_SCROLL";
-    public static final String SCROLL_Y_FIELD = "scrollY";
     public static final String STATE_FIELD = "state";
     public static final String SWITCHED_TO_STATE_MESSAGE = "TAPL_SWITCHED_TO_STATE";
     public static final String SCROLL_FINISHED_MESSAGE = "TAPL_SCROLL_FINISHED";
     public static final String PAUSE_DETECTED_MESSAGE = "TAPL_PAUSE_DETECTED";
-    public static final String RESPONSE_MESSAGE_POSTFIX = "_RESPONSE";
     public static final int NORMAL_STATE_ORDINAL = 0;
     public static final int SPRING_LOADED_STATE_ORDINAL = 1;
     public static final int OVERVIEW_STATE_ORDINAL = 2;
     public static final int OVERVIEW_PEEK_STATE_ORDINAL = 3;
-    public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
-    public static final int ALL_APPS_STATE_ORDINAL = 5;
-    public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+    public static final int OVERVIEW_MODAL_TASK_STATE_ORDINAL = 4;
+    public static final int QUICK_SWITCH_STATE_ORDINAL = 5;
+    public static final int ALL_APPS_STATE_ORDINAL = 6;
+    public static final int BACKGROUND_APP_STATE_ORDINAL = 7;
+    public static final int HINT_STATE_ORDINAL = 8;
+    public static final String TAPL_EVENTS_TAG = "TaplEvents";
+    public static final String SEQUENCE_MAIN = "Main";
+    public static final String SEQUENCE_TIS = "TIS";
+    public static final String SEQUENCE_PILFER = "Pilfer";
 
     public static String stateOrdinalToString(int ordinal) {
         switch (ordinal) {
@@ -45,14 +48,18 @@
                 return "Overview";
             case OVERVIEW_PEEK_STATE_ORDINAL:
                 return "OverviewPeek";
+            case OVERVIEW_MODAL_TASK_STATE_ORDINAL:
+                return "OverviewModal";
             case QUICK_SWITCH_STATE_ORDINAL:
                 return "QuickSwitch";
             case ALL_APPS_STATE_ORDINAL:
                 return "AllApps";
             case BACKGROUND_APP_STATE_ORDINAL:
                 return "Background";
+            case HINT_STATE_ORDINAL:
+                return "Hint";
             default:
-                return null;
+                return "Unknown";
         }
     }
 
@@ -73,18 +80,30 @@
     public static final String REQUEST_FREEZE_APP_LIST = "freeze-app-list";
     public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
     public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
-    public static final String REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN = "overview-left-margin";
-    public static final String REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN = "overview-right-margin";
+    public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
+    public static final String REQUEST_WINDOW_INSETS = "window-insets";
+    public static final String REQUEST_PID = "pid";
     public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
     public static final String REQUEST_JAVA_LEAK = "java-leak";
     public static final String REQUEST_NATIVE_LEAK = "native-leak";
+    public static final String REQUEST_VIEW_LEAK = "view-leak";
+    public static final String REQUEST_RECENT_TASKS_LIST = "recent-tasks-list";
+    public static final String REQUEST_START_EVENT_LOGGING = "start-event-logging";
+    public static final String REQUEST_GET_TEST_EVENTS = "get-test-events";
+    public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging";
+    public static final String REQUEST_CLEAR_DATA = "clear-data";
 
     public static boolean sDebugTracing = false;
     public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
 
-    public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
-    public static final String NO_DRAG_TO_WORKSPACE = "b/138729456";
-    public static final String APP_NOT_DISABLED = "b/139891609";
-    public static final String NO_CONTEXT_MENU = "b/141770616";
+    public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+
+    public static boolean sDisableSensorRotation;
+    public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
+
+    public static final String PERMANENT_DIAG_TAG = "TaplTarget";
+    public static final String PAUSE_NOT_DETECTED = "b/139891609";
+    public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
+    public static final String NO_SWIPE_TO_HOME = "b/158017601";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index f40f976..3c78b08 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -19,11 +19,13 @@
 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.LauncherStateManager.ANIM_ALL;
-import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_SCALE_COMPONENT;
-import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_NON_ATOMIC;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
@@ -31,22 +33,26 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.FlingBlockCheck;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.TouchController;
 
 /**
@@ -162,6 +168,9 @@
 
     @Override
     public final boolean onControllerTouchEvent(MotionEvent ev) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "onControllerTouchEvent");
+        }
         return mDetector.onTouchEvent(ev);
     }
 
@@ -176,7 +185,7 @@
     protected abstract LauncherState getTargetState(LauncherState fromState,
             boolean isDragTowardPositive);
 
-    protected abstract float initCurrentAnimation(@AnimationComponents int animComponents);
+    protected abstract float initCurrentAnimation(@AnimationFlags int animComponents);
 
     /**
      * Returns the container that the touch started from when leaving NORMAL state.
@@ -194,6 +203,10 @@
 
         mFromState = newFromState;
         mToState = newToState;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "reinitCurrentAnimation: "
+                    + newToState.ordinal + " " + getClass().getSimpleName());
+        }
 
         mStartProgress = 0;
         mPassedOverviewAtomicThreshold = false;
@@ -201,10 +214,10 @@
             mCurrentAnimation.setOnCancelRunnable(null);
         }
         int animComponents = goingBetweenNormalAndOverview(mFromState, mToState)
-                ? NON_ATOMIC_COMPONENT : ANIM_ALL;
+                ? PLAY_NON_ATOMIC : ANIM_ALL_COMPONENTS;
         mScheduleResumeAtomicComponent = false;
         if (mAtomicAnim != null) {
-            animComponents = NON_ATOMIC_COMPONENT;
+            animComponents = PLAY_NON_ATOMIC;
             // Control the non-atomic components until the atomic animation finishes, then control
             // the atomic components as well.
             mScheduleResumeAtomicComponent = true;
@@ -215,7 +228,7 @@
         }
 
         if (mAtomicComponentsController != null) {
-            animComponents &= ~ATOMIC_OVERVIEW_SCALE_COMPONENT;
+            animComponents &= ~PLAY_ATOMIC_OVERVIEW_SCALE;
         }
         mProgressMultiplier = initCurrentAnimation(animComponents);
         mCurrentAnimation.dispatchOnStart();
@@ -230,9 +243,10 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         mStartState = mLauncher.getStateManager().getState();
         mIsLogContainerSet = false;
+
         if (mCurrentAnimation == null) {
             mFromState = mStartState;
             mToState = null;
@@ -250,6 +264,10 @@
         }
         mCanBlockFling = mFromState == NORMAL;
         mFlingBlockCheck.unblockFling();
+        // Must be called after all the animation controllers have been paused
+        if (mToState == ALL_APPS || mToState == NORMAL) {
+            mLauncher.getAllAppsController().onDragStart(mToState == ALL_APPS);
+        }
     }
 
     @Override
@@ -284,11 +302,11 @@
     public boolean onDrag(float displacement, MotionEvent ev) {
         if (!mIsLogContainerSet) {
             if (mStartState == ALL_APPS) {
-                mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS;
+                mStartContainerType = ContainerType.ALLAPPS;
             } else if (mStartState == NORMAL) {
                 mStartContainerType = getLogContainerTypeForNormalState(ev);
             } else if (mStartState == OVERVIEW) {
-                mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER;
+                mStartContainerType = ContainerType.TASKSWITCHER;
             }
             mIsLogContainerSet = true;
         }
@@ -296,6 +314,9 @@
     }
 
     protected void updateProgress(float fraction) {
+        if (mCurrentAnimation == null) {
+            return;
+        }
         mCurrentAnimation.setPlayFraction(fraction);
         if (mAtomicComponentsController != null) {
             // Make sure we don't divide by 0, and have at least a small runway.
@@ -358,14 +379,18 @@
 
     private AnimatorSet createAtomicAnimForState(LauncherState fromState, LauncherState targetState,
             long duration) {
-        AnimatorSetBuilder builder = getAnimatorSetBuilderForStates(fromState, targetState);
-        return mLauncher.getStateManager().createAtomicAnimation(fromState, targetState, builder,
-                ATOMIC_OVERVIEW_SCALE_COMPONENT, duration);
+        StateAnimationConfig config = getConfigForStates(fromState, targetState);
+        config.animFlags = PLAY_ATOMIC_OVERVIEW_SCALE;
+        config.duration = duration;
+        return mLauncher.getStateManager().createAtomicAnimation(fromState, targetState, config);
     }
 
-    protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState,
-            LauncherState toState) {
-        return new AnimatorSetBuilder();
+    /**
+     * Returns animation config for state transition between provided states
+     */
+    protected StateAnimationConfig getConfigForStates(
+            LauncherState fromState, LauncherState toState) {
+        return new StateAnimationConfig();
     }
 
     @Override
@@ -380,6 +405,7 @@
 
         final LauncherState targetState;
         final float progress = mCurrentAnimation.getProgressFraction();
+        final float progressVelocity = velocity * mProgressMultiplier;
         final float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
         if (fling) {
             targetState =
@@ -406,7 +432,7 @@
                 startProgress = 1;
             } else {
                 startProgress = Utilities.boundToRange(progress
-                        + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
+                        + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
                 duration = BaseSwipeDetector.calculateDuration(velocity,
                         endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
@@ -421,7 +447,7 @@
                 startProgress = 0;
             } else {
                 startProgress = Utilities.boundToRange(progress
-                        + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
+                        + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
                 duration = BaseSwipeDetector.calculateDuration(velocity,
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
@@ -433,8 +459,8 @@
         maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
         updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
                 targetState, velocity, fling);
-        mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, velocity);
-        if (fling && targetState == LauncherState.ALL_APPS && !QUICKSTEP_SPRINGS.get()) {
+        mCurrentAnimation.dispatchOnStart();
+        if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
         }
         anim.start();
@@ -508,7 +534,7 @@
             mAtomicComponentsController.getAnimationPlayer().end();
             mAtomicComponentsController = null;
         }
-        cancelAnimationControllers();
+        clearState();
         boolean shouldGoToTargetState = true;
         if (mPendingAnimation != null) {
             boolean reachedTarget = mToState == targetState;
@@ -525,18 +551,34 @@
         if (targetState != mStartState) {
             logReachedState(logAction, targetState);
         }
-        mLauncher.getStateManager().goToState(targetState, false /* animated */);
-        mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(1, 0, 0);
+        if (!mLauncher.isInState(targetState)) {
+            // If we're already in the target state, don't jump to it at the end of the animation in
+            // 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();
     }
 
     private void logReachedState(int logAction, LauncherState targetState) {
         // Transition complete. log the action
         mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
                 getDirectionForLog(), mDetector.getDownX(), mDetector.getDownY(),
-                mStartContainerType,
-                mStartState.containerType,
+                mStartContainerType /* e.g., hotseat */,
+                mStartState.containerType /* e.g., workspace */,
                 targetState.containerType,
                 mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStatsLogManager().logger()
+                .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
+                .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
+                .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                        .setWorkspace(
+                                LauncherAtom.WorkspaceContainer.newBuilder()
+                                        .setPageIndex(mLauncher.getWorkspace().getCurrentPage()))
+                        .build())
+                .log(StatsLogManager.getLauncherAtomEvent(mStartState.containerType,
+                            targetState.containerType, mToState.ordinal > mFromState.ordinal
+                                    ? LAUNCHER_UNKNOWN_SWIPEUP
+                                    : LAUNCHER_UNKNOWN_SWIPEDOWN));
     }
 
     protected void clearState() {
@@ -546,13 +588,13 @@
             mAtomicAnim = null;
         }
         mScheduleResumeAtomicComponent = false;
+        mDetector.finishedScrolling();
+        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAnimationControllers() {
         mCurrentAnimation = null;
         cancelAtomicComponentsController();
-        mDetector.finishedScrolling();
-        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAtomicComponentsController() {
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
similarity index 72%
rename from src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
rename to src/com/android/launcher3/touch/AllAppsSwipeController.java
index 23f21a1..4a202b6 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -1,4 +1,19 @@
-package com.android.launcher3.uioverrides;
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.touch;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -8,9 +23,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.AnimationComponents;
-import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**
@@ -58,12 +71,12 @@
 
     @Override
     protected int getLogContainerTypeForNormalState(MotionEvent ev) {
-        return mLauncher.getDragLayer().isEventOverView(mLauncher.getHotseat(), mTouchDownEvent) ?
-                ContainerType.HOTSEAT : ContainerType.WORKSPACE;
+        return mLauncher.getDragLayer().isEventOverView(mLauncher.getHotseat(), mTouchDownEvent)
+                ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
     }
 
     @Override
-    protected float initCurrentAnimation(@AnimationComponents int animComponents) {
+    protected float initCurrentAnimation(@AnimationFlags int animComponents) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
         mCurrentAnimation = mLauncher.getStateManager()
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 12ca5ee..01b33d8 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -24,6 +24,12 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.LinkedList;
+import java.util.Queue;
 
 /**
  * Scroll/drag/swipe gesture detector.
@@ -49,13 +55,15 @@
     protected final boolean mIsRtl;
     protected final float mTouchSlop;
     protected final float mMaxVelocity;
+    private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
 
     private int mActivePointerId = INVALID_POINTER_ID;
     private VelocityTracker mVelocityTracker;
     private PointF mLastDisplacement = new PointF();
     private PointF mDisplacement = new PointF();
     protected PointF mSubtractDisplacement = new PointF();
-    private ScrollState mState = ScrollState.IDLE;
+    @VisibleForTesting ScrollState mState = ScrollState.IDLE;
+    private boolean mIsSettingState;
 
     protected boolean mIgnoreSlopWhenSettling;
 
@@ -167,6 +175,9 @@
                 if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
                     setState(ScrollState.DRAGGING);
                 }
+                if (TestProtocol.sDebugTracing) {
+                    Log.d(TestProtocol.PAUSE_NOT_DETECTED, "before report dragging");
+                }
                 if (mState == ScrollState.DRAGGING) {
                     reportDragging(ev);
                 }
@@ -195,6 +206,12 @@
     // SETTLING -> (View settled) -> IDLE
 
     private void setState(ScrollState newState) {
+        if (mIsSettingState) {
+            mSetStateQueue.add(() -> setState(newState));
+            return;
+        }
+        mIsSettingState = true;
+
         if (DBG) {
             Log.d(TAG, "setState:" + mState + "->" + newState);
         }
@@ -212,6 +229,10 @@
         }
 
         mState = newState;
+        mIsSettingState = false;
+        if (!mSetStateQueue.isEmpty()) {
+            mSetStateQueue.remove().run();
+        }
     }
 
     private void initializeDragging() {
@@ -220,7 +241,7 @@
         } else {
             mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
             mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
-        }
+        } 
     }
 
     protected abstract boolean shouldScrollStart(PointF displacement);
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 03493a5..8486666 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -15,18 +15,17 @@
  */
 package com.android.launcher3.touch;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 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_FOLDER_OPEN;
 import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 
 import android.app.AlertDialog;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller.SessionInfo;
@@ -40,25 +39,28 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.logging.StatsLogManager;
+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.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 /**
  * Class for handling clicks on workspace and all-apps items
@@ -111,6 +113,8 @@
         if (!folder.isOpen() && !folder.isDestroyed()) {
             // Open the requested folder
             folder.animateOpen();
+            StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo)
+                    .log(LAUNCHER_FOLDER_OPEN);
         }
     }
 
@@ -125,8 +129,8 @@
 
         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
         if (v.isReadyForClickSetup()) {
-            LauncherAppWidgetProviderInfo appWidgetInfo = AppWidgetManagerCompat
-                    .getInstance(launcher).findProvider(info.providerName, info.user);
+            LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher)
+                    .findProvider(info.providerName, info.user);
             if (appWidgetInfo == null) {
                 return;
             }
@@ -171,8 +175,8 @@
     private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) {
         ItemInfo item = (ItemInfo) v.getTag();
         if (Utilities.ATLEAST_Q) {
-            PackageInstallerCompat pkgInstaller = PackageInstallerCompat.getInstance(launcher);
-            SessionInfo sessionInfo = pkgInstaller.getActiveSessionInfo(item.user, packageName);
+            SessionInfo sessionInfo = InstallSessionHelper.INSTANCE.get(launcher)
+                    .getActiveSessionInfo(item.user, packageName);
             if (sessionInfo != null) {
                 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
                 try {
@@ -226,8 +230,9 @@
 
         // Check for abandoned promise
         if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
-            String packageName = shortcut.intent.getComponent() != null ?
-                    shortcut.intent.getComponent().getPackageName() : shortcut.intent.getPackage();
+            String packageName = shortcut.getIntent().getComponent() != null
+                    ? shortcut.getIntent().getComponent().getPackageName()
+                    : shortcut.getIntent().getPackage();
             if (!TextUtils.isEmpty(packageName)) {
                 onClickPendingAppItem(v, launcher, packageName,
                         shortcut.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE));
@@ -241,6 +246,8 @@
 
     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher,
             @Nullable String sourceContainer) {
+        TestLogging.recordEvent(
+                TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity");
         Intent intent;
         if (item instanceof PromiseAppInfo) {
             PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 86d2b39..7baeab8 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -22,34 +22,33 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
-import android.util.Log;
 import android.view.View;
 import android.view.View.OnLongClickListener;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 
-import java.util.Arrays;
-
 /**
  * Class to handle long-clicks on workspace items and start drag as a result.
  */
 public class ItemLongClickListener {
 
-    public static OnLongClickListener INSTANCE_WORKSPACE =
+    public static final OnLongClickListener INSTANCE_WORKSPACE =
             ItemLongClickListener::onWorkspaceItemLongClick;
 
-    public static OnLongClickListener INSTANCE_ALL_APPS =
+    public static final OnLongClickListener INSTANCE_ALL_APPS =
             ItemLongClickListener::onAllAppsItemLongClick;
 
     private static boolean onWorkspaceItemLongClick(View v) {
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onWorkspaceItemLongClick");
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
@@ -79,19 +78,12 @@
     }
 
     private static boolean onAllAppsItemLongClick(View v) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick1");
-        }
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onAllAppsItemLongClick");
+        v.cancelLongPress();
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick2");
-        }
         // When we have exited all apps or are in transition, disregard long clicks
         if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick3");
-        }
         if (launcher.getWorkspace().isSwitchingState()) return false;
 
         // Start the drag
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
new file mode 100644
index 0000000..48c7734
--- /dev/null
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import static android.widget.ListPopupWindow.WRAP_CONTENT;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.PagedView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.OverScroller;
+
+public class LandscapePagedViewHandler implements PagedOrientationHandler {
+
+    @Override
+    public int getPrimaryValue(int x, int y) {
+        return y;
+    }
+
+    @Override
+    public int getSecondaryValue(int x, int y) {
+        return x;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int minMaxScroll) {
+        pagedView.superScrollTo(secondaryScroll, minMaxScroll);
+    }
+
+    @Override
+    public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
+        pagedView.scrollTo(pagedView.getScrollX() + x, unboundedScroll + y);
+    }
+
+    @Override
+    public void scrollerStartScroll(OverScroller scroller, int newPosition) {
+        scroller.startScroll(scroller.getCurrPos(), newPosition - scroller.getCurrPos());
+    }
+
+    @Override
+    public void getCurveProperties(PagedView view, Rect insets, CurveProperties out) {
+        out.scroll = view.getScrollY();
+        out.halfPageSize = view.getNormalChildHeight() / 2;
+        out.halfScreenSize = view.getMeasuredHeight() / 2;
+        out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement < 0 : displacement > 0;
+    }
+
+    @Override
+    public boolean isLayoutNaturalToLauncher() {
+        return false;
+    }
+
+    @Override
+    public void adjustFloatingIconStartVelocity(PointF velocity) {
+        float oldX = velocity.x;
+        float oldY = velocity.y;
+        velocity.set(-oldY, oldX);
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
+        pagedView.superScrollTo(pagedView.getScrollX(), primaryScroll);
+    }
+
+    @Override
+    public <T> void set(T target, Int2DAction<T> action, int param) {
+        action.call(target, 0, param);
+    }
+
+    @Override
+    public <T> void set(T target, Float2DAction<T> action, float param) {
+        action.call(target, 0, param);
+    }
+
+    @Override
+    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
+        return event.getY(pointerIndex);
+    }
+
+    @Override
+    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
+        return velocityTracker.getYVelocity(pointerId);
+    }
+
+    @Override
+    public int getMeasuredSize(View view) {
+        return view.getMeasuredHeight();
+    }
+
+    @Override
+    public float getPrimarySize(RectF rect) {
+        return rect.height();
+    }
+
+    @Override
+    public int getClearAllScrollOffset(View view, boolean isRtl) {
+        return (isRtl ? view.getPaddingBottom() : - view.getPaddingTop()) / 2;
+    }
+
+    @Override
+    public int getSecondaryDimension(View view) {
+        return view.getWidth();
+    }
+
+    @Override
+    public FloatProperty<View> getPrimaryViewTranslate() {
+        return VIEW_TRANSLATE_Y;
+    }
+
+    @Override
+    public FloatProperty<View> getSecondaryViewTranslate() {
+        return VIEW_TRANSLATE_X;
+    }
+
+    @Override
+    public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
+        view.setTranslationX(0);
+        view.setTranslationY(translation);
+    }
+
+    @Override
+    public int getPrimaryScroll(View view) {
+        return view.getScrollY();
+    }
+
+    @Override
+    public float getPrimaryScale(View view) {
+        return view.getScaleY();
+    }
+
+    @Override
+    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
+        event.setMaxScrollY(maxScroll);
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return !Utilities.isRtl(resources);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 90;
+    }
+
+    @Override
+    public int getRotation() {
+        return Surface.ROTATION_90;
+    }
+
+    @Override
+    public int getChildStart(View view) {
+        return view.getTop();
+    }
+
+    @Override
+    public float getChildStartWithTranslation(View view) {
+        return view.getTop() + view.getTranslationY();
+    }
+
+    @Override
+    public int getCenterForPage(View view, Rect insets) {
+        return (view.getPaddingLeft() + view.getMeasuredWidth() + insets.left
+            - insets.right - view.getPaddingRight()) / 2;
+    }
+
+    @Override
+    public int getScrollOffsetStart(View view, Rect insets) {
+        return insets.top + view.getPaddingTop();
+    }
+
+    @Override
+    public int getScrollOffsetEnd(View view, Rect insets) {
+        return view.getHeight() - view.getPaddingBottom() - insets.bottom;
+    }
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
+        return HORIZONTAL;
+    }
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return 1;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        return isRtl ? 1 : -1;
+    }
+
+    @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return thumbnailView.getMeasuredWidth() + x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y;
+    }
+
+    @Override
+    public int getTaskMenuWidth(View view) {
+        return view.getMeasuredHeight();
+    }
+
+    @Override
+    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
+        return LinearLayout.HORIZONTAL;
+    }
+
+    @Override
+    public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
+        lp.width = 0;
+        lp.height = WRAP_CONTENT;
+        lp.weight = 1;
+    }
+
+    @Override
+    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
+        boolean layoutChild) {
+        final int childHeight = child.getMeasuredHeight();
+        final int childBottom = childStart + childHeight;
+        final int childWidth = child.getMeasuredWidth();
+        final int childLeft = pageCenter - childWidth/ 2;
+        if (layoutChild) {
+            child.layout(childLeft, childStart, childLeft + childWidth, childBottom);
+        }
+        return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
+    }
+}
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
new file mode 100644
index 0000000..65b1a7a
--- /dev/null
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -0,0 +1,128 @@
+/*
+ * 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.touch;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.util.OverScroller;
+
+/**
+ * Abstraction layer to separate horizontal and vertical specific implementations
+ * for {@link com.android.launcher3.PagedView}. Majority of these implementations are (should be) as
+ * simple as choosing the correct X and Y analogous methods.
+ */
+public interface PagedOrientationHandler {
+
+    PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
+    PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
+    PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
+
+    interface Int2DAction<T> {
+        void call(T target, int x, int y);
+    }
+    interface Float2DAction<T> {
+        void call(T target, float x, float y);
+    }
+    Int2DAction<View> VIEW_SCROLL_BY = View::scrollBy;
+    Int2DAction<View> VIEW_SCROLL_TO = View::scrollTo;
+    Float2DAction<Canvas> CANVAS_TRANSLATE = Canvas::translate;
+    Float2DAction<Matrix> MATRIX_POST_TRANSLATE = Matrix::postTranslate;
+
+    <T> void set(T target, Int2DAction<T> action, int param);
+    <T> void set(T target, Float2DAction<T> action, float param);
+    float getPrimaryDirection(MotionEvent event, int pointerIndex);
+    float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
+    int getMeasuredSize(View view);
+    float getPrimarySize(RectF rect);
+    int getClearAllScrollOffset(View view, boolean isRtl);
+    int getSecondaryDimension(View view);
+    FloatProperty<View> getPrimaryViewTranslate();
+    FloatProperty<View> getSecondaryViewTranslate();
+    void setPrimaryAndResetSecondaryTranslate(View view, float translation);
+    int getPrimaryScroll(View view);
+    float getPrimaryScale(View view);
+    int getChildStart(View view);
+    float getChildStartWithTranslation(View view);
+    int getCenterForPage(View view, Rect insets);
+    int getScrollOffsetStart(View view, Rect insets);
+    int getScrollOffsetEnd(View view, Rect insets);
+    SingleAxisSwipeDetector.Direction getOppositeSwipeDirection();
+    int getTaskDismissDirectionFactor();
+    int getTaskDragDisplacementFactor(boolean isRtl);
+    ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
+    void setMaxScroll(AccessibilityEvent event, int maxScroll);
+    boolean getRecentsRtlSetting(Resources resources);
+    float getDegreesRotated();
+    int getRotation();
+    int getPrimaryValue(int x, int y);
+    int getSecondaryValue(int x, int y);
+    void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll);
+    /** Uses {@params pagedView}.getScroll[X|Y]() method for the secondary amount*/
+    void delegateScrollTo(PagedView pagedView, int primaryScroll);
+    void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y);
+    void scrollerStartScroll(OverScroller scroller, int newPosition);
+    void getCurveProperties(PagedView view, Rect insets, CurveProperties out);
+    boolean isGoingUp(float displacement, boolean isRtl);
+    boolean isLayoutNaturalToLauncher();
+    float getTaskMenuX(float x, View thumbnailView);
+    float getTaskMenuY(float y, View thumbnailView);
+    int getTaskMenuWidth(View view);
+    int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
+    void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
+
+    /**
+     * Maps the velocity from the coordinate plane of the foreground app to that
+     * of Launcher's (which now will always be portrait)
+     */
+    void adjustFloatingIconStartVelocity(PointF velocity);
+
+    class CurveProperties {
+        public int scroll;
+        public int halfPageSize;
+        public int screenCenter;
+        public int halfScreenSize;
+    }
+
+    class ChildBounds {
+
+        public final int primaryDimension;
+        public final int secondaryDimension;
+        public final int childPrimaryEnd;
+        public final int childSecondaryEnd;
+
+        ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd,
+            int childSecondaryEnd) {
+            this.primaryDimension = primaryDimension;
+            this.secondaryDimension = secondaryDimension;
+            this.childPrimaryEnd = childPrimaryEnd;
+            this.childSecondaryEnd = childSecondaryEnd;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
new file mode 100644
index 0000000..79e5c87
--- /dev/null
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.PagedView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.OverScroller;
+
+public class PortraitPagedViewHandler implements PagedOrientationHandler {
+
+    @Override
+    public int getPrimaryValue(int x, int y) {
+        return x;
+    }
+
+    @Override
+    public int getSecondaryValue(int x, int y) {
+        return y;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll) {
+        pagedView.superScrollTo(primaryScroll, secondaryScroll);
+    }
+
+    @Override
+    public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
+        pagedView.scrollTo(unboundedScroll + x, pagedView.getScrollY() + y);
+    }
+
+    @Override
+    public void scrollerStartScroll(OverScroller scroller, int newPosition) {
+        scroller.startScroll(newPosition - scroller.getCurrPos(), scroller.getCurrPos());
+    }
+
+    @Override
+    public void getCurveProperties(PagedView view, Rect insets, CurveProperties out) {
+        out.scroll = view.getScrollX();
+        out.halfPageSize = view.getNormalChildWidth() / 2;
+        out.halfScreenSize = view.getMeasuredWidth() / 2;
+        out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return displacement < 0;
+    }
+
+    @Override
+    public boolean isLayoutNaturalToLauncher() {
+        return true;
+    }
+
+    @Override
+    public void adjustFloatingIconStartVelocity(PointF velocity) {
+        //no-op
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
+        pagedView.superScrollTo(primaryScroll, pagedView.getScrollY());
+    }
+
+    @Override
+    public <T> void set(T target, Int2DAction<T> action, int param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public <T> void set(T target, Float2DAction<T> action, float param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
+        return event.getX(pointerIndex);
+    }
+
+    @Override
+    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
+        return velocityTracker.getXVelocity(pointerId);
+    }
+
+    @Override
+    public int getMeasuredSize(View view) {
+        return view.getMeasuredWidth();
+    }
+
+    @Override
+    public float getPrimarySize(RectF rect) {
+        return rect.width();
+    }
+
+    @Override
+    public int getClearAllScrollOffset(View view, boolean isRtl) {
+        return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2;
+    }
+
+    @Override
+    public int getSecondaryDimension(View view) {
+        return view.getHeight();
+    }
+
+    @Override
+    public FloatProperty<View> getPrimaryViewTranslate() {
+        return VIEW_TRANSLATE_X;
+    }
+
+    @Override
+    public FloatProperty<View> getSecondaryViewTranslate() {
+        return VIEW_TRANSLATE_Y;
+    }
+
+    @Override
+    public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
+        view.setTranslationX(translation);
+        view.setTranslationY(0);
+    }
+
+    @Override
+    public int getPrimaryScroll(View view) {
+        return view.getScrollX();
+    }
+
+    @Override
+    public float getPrimaryScale(View view) {
+        return view.getScaleX();
+    }
+
+    @Override
+    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
+        event.setMaxScrollX(maxScroll);
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return !Utilities.isRtl(resources);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 0;
+    }
+
+    @Override
+    public int getRotation() {
+        return Surface.ROTATION_0;
+    }
+
+    @Override
+    public int getChildStart(View view) {
+        return view.getLeft();
+    }
+
+    @Override
+    public float getChildStartWithTranslation(View view) {
+        return view.getLeft() + view.getTranslationX();
+    }
+
+    @Override
+    public int getCenterForPage(View view, Rect insets) {
+        return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top
+            - insets.bottom - view.getPaddingBottom()) / 2;
+    }
+
+    @Override
+    public int getScrollOffsetStart(View view, Rect insets) {
+        return insets.left + view.getPaddingLeft();
+    }
+
+    @Override
+    public int getScrollOffsetEnd(View view, Rect insets) {
+        return view.getWidth() - view.getPaddingRight() - insets.right;
+    }
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
+        return VERTICAL;
+    }
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return -1;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return 1;
+    }
+
+    @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y;
+    }
+
+    @Override
+    public int getTaskMenuWidth(View view) {
+        return view.getMeasuredWidth();
+    }
+
+    @Override
+    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
+        return taskMenuLayout.getOrientation();
+    }
+
+    @Override
+    public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
+        // no-op, defaults are fine
+    }
+
+    @Override
+    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
+        boolean layoutChild) {
+        final int childWidth = child.getMeasuredWidth();
+        final int childRight = childStart + childWidth;
+        final int childHeight = child.getMeasuredHeight();
+        final int childTop = pageCenter - childHeight / 2;
+        if (layoutChild) {
+            child.layout(childStart, childTop, childRight, childTop + childHeight);
+        }
+        return new ChildBounds(childWidth, childHeight, childRight, childTop);
+    }
+}
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
new file mode 100644
index 0000000..d5ae2dc
--- /dev/null
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.view.Surface;
+import android.view.View;
+
+import com.android.launcher3.Utilities;
+
+public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return -1;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        return isRtl ? -1 : 1;
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return Utilities.isRtl(resources);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 270;
+    }
+
+    @Override
+    public int getRotation() {
+        return Surface.ROTATION_270;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement > 0 : displacement < 0;
+    }
+
+    @Override
+    public void adjustFloatingIconStartVelocity(PointF velocity) {
+        float oldX = velocity.x;
+        float oldY = velocity.y;
+        velocity.set(oldY, -oldX);
+    }
+
+    @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y + thumbnailView.getMeasuredHeight();
+    }
+
+    @Override
+    public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
+        view.setTranslationX(0);
+        view.setTranslationY(translation);
+    }
+}
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index f2ebc45..875eefb 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
@@ -24,6 +25,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
 
 /**
  * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
@@ -54,8 +56,8 @@
         }
 
         @Override
-        boolean canScrollStart(PointF displacement, float touchSlop) {
-            return Math.abs(displacement.y) >= Math.max(Math.abs(displacement.x), touchSlop);
+        float extractOrthogonalDirection(PointF direction) {
+            return direction.x;
         }
 
     };
@@ -80,9 +82,10 @@
         }
 
         @Override
-        boolean canScrollStart(PointF displacement, float touchSlop) {
-            return Math.abs(displacement.x) >= Math.max(Math.abs(displacement.y), touchSlop);
+        float extractOrthogonalDirection(PointF direction) {
+            return direction.y;
         }
+
     };
 
     private final Direction mDir;
@@ -102,6 +105,11 @@
         super(config, isRtl);
         mListener = l;
         mDir = dir;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector.ctor "
+                    + l.getClass().getSimpleName()
+                    + " @ " + android.util.Log.getStackTraceString(new Throwable()));
+        }
     }
 
     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
@@ -126,7 +134,9 @@
     @Override
     protected boolean shouldScrollStart(PointF displacement) {
         // Reject cases where the angle or slop condition is not met.
-        if (!mDir.canScrollStart(displacement, mTouchSlop)) {
+        float minDisplacement = Math.max(mTouchSlop,
+                Math.abs(mDir.extractOrthogonalDirection(displacement)));
+        if (Math.abs(mDir.extractDirection(displacement)) < minDisplacement) {
             return false;
         }
 
@@ -145,12 +155,18 @@
 
     @Override
     protected void reportDragStartInternal(boolean recatch) {
-        mListener.onDragStart(!recatch);
+        float startDisplacement = mDir.extractDirection(mSubtractDisplacement);
+        mListener.onDragStart(!recatch, startDisplacement);
     }
 
     @Override
     protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
-        mListener.onDrag(mDir.extractDirection(displacement), event);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector "
+                    + mListener.getClass().getSimpleName());
+        }
+        mListener.onDrag(mDir.extractDirection(displacement),
+                mDir.extractOrthogonalDirection(displacement), event);
     }
 
     @Override
@@ -161,16 +177,24 @@
 
     /** Listener to receive updates on the swipe. */
     public interface Listener {
-        /** @param start whether this was the original drag start, as opposed to a recatch. */
-        void onDragStart(boolean start);
+        /**
+         * TODO(b/150256055) consolidate all the different onDrag() methods into one
+         * @param start whether this was the original drag start, as opposed to a recatch.
+         * @param startDisplacement the initial touch displacement for the primary direction as
+         *        given by by {@link Direction#extractDirection(PointF)}
+         */
+        void onDragStart(boolean start, float startDisplacement);
 
-        // TODO remove
         boolean onDrag(float displacement);
 
         default boolean onDrag(float displacement, MotionEvent event) {
             return onDrag(displacement);
         }
 
+        default boolean onDrag(float displacement, float orthogonalDisplacement, MotionEvent ev) {
+            return onDrag(displacement, ev);
+        }
+
         void onDragEnd(float velocity);
     }
 
@@ -183,8 +207,7 @@
         /** Returns the part of the given {@link PointF} that is relevant to this direction. */
         abstract float extractDirection(PointF point);
 
-        /** Reject cases where the angle or slop condition is not met. */
-        abstract boolean canScrollStart(PointF displacement, float touchSlop);
+        abstract float extractOrthogonalDirection(PointF point);
 
     }
 }
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 310d598..4fa658e 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -22,6 +22,7 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_LONGPRESS;
 
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -38,9 +39,9 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.OptionsPopupView;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**
  * Helper class to handle touch on empty space in workspace and show options popup on long press
@@ -166,15 +167,14 @@
     @Override
     public void onLongPress(MotionEvent event) {
         if (mLongPressState == STATE_REQUESTED) {
+            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Workspace.longPress");
             if (canHandleLongPress()) {
                 mLongPressState = STATE_PENDING_PARENT_INFORM;
                 mWorkspace.getParent().requestDisallowInterceptTouchEvent(true);
 
                 mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
-                mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
-                        Action.Direction.NONE, ContainerType.WORKSPACE,
-                        mWorkspace.getCurrentPage());
+                mLauncher.getStatsLogManager().logger().log(LAUNCHER_WORKSPACE_LONGPRESS);
                 OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
             } else {
                 cancelLongPress();
diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
new file mode 100644
index 0000000..59266b4
--- /dev/null
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Helper class to statically track activity creation
+ * @param <T> The activity type to track
+ */
+public final class ActivityTracker<T extends BaseActivity> {
+
+    private WeakReference<T> mCurrentActivity = new WeakReference<>(null);
+
+    private static final String EXTRA_SCHEDULER_CALLBACK = "launcher.scheduler_callback";
+
+    @Nullable
+    public <R extends T> R getCreatedActivity() {
+        return (R) mCurrentActivity.get();
+    }
+
+    public void onActivityDestroyed(T activity) {
+        if (mCurrentActivity.get() == activity) {
+            mCurrentActivity.clear();
+        }
+    }
+
+    /**
+     * Call {@link SchedulerCallback#init(BaseActivity, boolean)} when the activity is ready.
+     * If the activity is already created, this is called immediately, otherwise we add the
+     * callback as an extra on the intent, and will call init() when we get handleIntent().
+     * @param callback The callback to call init() on when the activity is ready.
+     * @param intent The intent that will be used to initialize the activity, if the activity
+     *               doesn't already exist. We add the callback as an extra on this intent.
+     */
+    public void runCallbackWhenActivityExists(SchedulerCallback<T> callback, Intent intent) {
+        T activity = mCurrentActivity.get();
+        if (activity != null) {
+            callback.init(activity, activity.isStarted());
+        } else {
+            callback.addToIntent(intent);
+        }
+    }
+
+    public boolean handleCreate(T activity) {
+        mCurrentActivity = new WeakReference<>(activity);
+        return handleIntent(activity, activity.getIntent(), false);
+    }
+
+    public boolean handleNewIntent(T activity, Intent intent) {
+        return handleIntent(activity, intent, activity.isStarted());
+    }
+
+    private boolean handleIntent(T activity, Intent intent, boolean alreadyOnHome) {
+        if (intent != null && intent.getExtras() != null) {
+            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_SCHEDULER_CALLBACK);
+            if (stateBinder instanceof ObjectWrapper) {
+                SchedulerCallback<T> handler =
+                        ((ObjectWrapper<SchedulerCallback>) stateBinder).get();
+                if (!handler.init(activity, alreadyOnHome)) {
+                    intent.getExtras().remove(EXTRA_SCHEDULER_CALLBACK);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public interface SchedulerCallback<T extends BaseActivity> {
+
+        /**
+         * Called when the activity is ready.
+         * @param alreadyOnHome Whether the activity is already started.
+         * @return Whether to continue receiving callbacks (i.e. if the activity is recreated).
+         */
+        boolean init(T activity, boolean alreadyOnHome);
+
+        /**
+         * Adds this callback as an extra on the intent, so we can retrieve it in handleIntent() and
+         * call {@link #init}. The intent should be used to start the activity after calling this
+         * method in order for us to get handleIntent().
+         */
+        default Intent addToIntent(Intent intent) {
+            Bundle extras = new Bundle();
+            extras.putBinder(EXTRA_SCHEDULER_CALLBACK, ObjectWrapper.wrap(this));
+            intent.putExtras(extras);
+            return intent;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 00adf10..30c9ff9 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -19,14 +19,14 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.pm.UserCache;
 
 /**
  * A wrapper around {@link ContentValues} with some utility methods.
@@ -37,7 +37,7 @@
     private final Context mContext;
 
     private CommitParams mCommitParams;
-    private Bitmap mIcon;
+    private BitmapInfo mIcon;
     private UserHandle mUser;
 
     public ContentWriter(Context context, CommitParams commitParams) {
@@ -79,14 +79,14 @@
         return this;
     }
 
-    public ContentWriter putIcon(Bitmap value, UserHandle user) {
+    public ContentWriter putIcon(BitmapInfo value, UserHandle user) {
         mIcon = value;
         mUser = user;
         return this;
     }
 
     public ContentWriter put(String key, UserHandle user) {
-        return put(key, UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user));
+        return put(key, UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user));
     }
 
     /**
@@ -97,7 +97,7 @@
         Preconditions.assertNonUiThread();
         if (mIcon != null && !LauncherAppState.getInstance(context).getIconCache()
                 .isDefaultIcon(mIcon, mUser)) {
-            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon));
+            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon.icon));
             mIcon = null;
         }
         return mValues;
diff --git a/src/com/android/launcher3/util/DefaultDisplay.java b/src/com/android/launcher3/util/DefaultDisplay.java
index 8529d50..35788a5 100644
--- a/src/com/android/launcher3/util/DefaultDisplay.java
+++ b/src/com/android/launcher3/util/DefaultDisplay.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.util;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.content.Context;
@@ -26,7 +28,8 @@
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
-import android.view.WindowManager;
+
+import androidx.annotation.VisibleForTesting;
 
 import java.util.ArrayList;
 
@@ -44,20 +47,27 @@
     public static final int CHANGE_ROTATION = 1 << 1;
     public static final int CHANGE_FRAME_DELAY = 1 << 2;
 
-    private final Context mContext;
+    public static final int CHANGE_ALL = CHANGE_SIZE | CHANGE_ROTATION | CHANGE_FRAME_DELAY;
+
+    private final Context mDisplayContext;
     private final int mId;
     private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
     private final Handler mChangeHandler;
     private Info mInfo;
 
     private DefaultDisplay(Context context) {
-        mContext = context;
-        mInfo = new Info(context);
+        DisplayManager dm = context.getSystemService(DisplayManager.class);
+        // Use application context to create display context so that it can have its own Resources.
+        mDisplayContext = context.getApplicationContext().createDisplayContext(
+                dm.getDisplay(DEFAULT_DISPLAY));
+        // Note that the Display object must be obtained from DisplayManager which is associated to
+        // the display context, so the Display is isolated from Activity and Application to provide
+        // the actual state of device that excludes the additional adjustment and override.
+        mInfo = new Info(mDisplayContext);
         mId = mInfo.id;
         mChangeHandler = new Handler(this::onChange);
 
-        context.getSystemService(DisplayManager.class)
-                .registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
+        dm.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
     }
 
     @Override
@@ -73,7 +83,7 @@
         }
 
         Info oldInfo = mInfo;
-        Info info = new Info(mContext);
+        Info info = new Info(mDisplayContext);
 
         int change = 0;
         if (info.hasDifferentSize(oldInfo)) {
@@ -127,9 +137,24 @@
 
         public final DisplayMetrics metrics;
 
-        private Info(Context context) {
-            Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
+        @VisibleForTesting
+        public Info(int id, int rotation, int singleFrameMs, Point realSize, Point smallestSize,
+                Point largestSize, DisplayMetrics metrics) {
+            this.id = id;
+            this.rotation = rotation;
+            this.singleFrameMs = singleFrameMs;
+            this.realSize = realSize;
+            this.smallestSize = smallestSize;
+            this.largestSize = largestSize;
+            this.metrics = metrics;
+        }
 
+        private Info(Context context) {
+            this(context, context.getSystemService(DisplayManager.class)
+                    .getDisplay(DEFAULT_DISPLAY));
+        }
+
+        public Info(Context context, Display display) {
             id = display.getDisplayId();
             rotation = display.getRotation();
 
@@ -142,8 +167,7 @@
             display.getRealSize(realSize);
             display.getCurrentSizeRange(smallestSize, largestSize);
 
-            metrics = new DisplayMetrics();
-            display.getMetrics(metrics);
+            metrics = context.getResources().getDisplayMetrics();
         }
 
         private boolean hasDifferentSize(Info info) {
diff --git a/src/com/android/launcher3/util/DynamicResource.java b/src/com/android/launcher3/util/DynamicResource.java
new file mode 100644
index 0000000..e6ee186
--- /dev/null
+++ b/src/com/android/launcher3/util/DynamicResource.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Context;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.FractionRes;
+import androidx.annotation.IntegerRes;
+
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.ResourceProvider;
+
+/**
+ * Utility class to support customizing resource values using plugins
+ *
+ * To load resources, call
+ *    DynamicResource.provider(context).getInt(resId) or any other supported methods
+ *
+ * To allow customization for a particular resource, add them to dynamic_resources.xml
+ */
+public class DynamicResource implements ResourceProvider, PluginListener<ResourceProvider> {
+
+    private static final MainThreadInitializedObject<DynamicResource> INSTANCE =
+            new MainThreadInitializedObject<>(DynamicResource::new);
+
+    private final Context mContext;
+    private ResourceProvider mPlugin;
+
+    private DynamicResource(Context context) {
+        mContext = context;
+        PluginManagerWrapper.INSTANCE.get(context).addPluginListener(this,
+                ResourceProvider.class, false /* allowedMultiple */);
+    }
+
+    @Override
+    public int getInt(@IntegerRes int resId) {
+        return mContext.getResources().getInteger(resId);
+    }
+
+    @Override
+    public float getFraction(@FractionRes int resId) {
+        return mContext.getResources().getFraction(resId, 1, 1);
+    }
+
+    @Override
+    public float getDimension(@DimenRes int resId) {
+        return mContext.getResources().getDimension(resId);
+    }
+
+    @Override
+    public int getColor(@ColorRes int resId) {
+        return mContext.getResources().getColor(resId, null);
+    }
+
+    @Override
+    public float getFloat(@DimenRes int resId) {
+        return mContext.getResources().getFloat(resId);
+    }
+
+    @Override
+    public void onPluginConnected(ResourceProvider plugin, Context context) {
+        mPlugin = plugin;
+    }
+
+    @Override
+    public void onPluginDisconnected(ResourceProvider plugin) {
+        mPlugin = null;
+    }
+
+    /**
+     * Returns the currently active or default provider
+     */
+    public static ResourceProvider provider(Context context) {
+        DynamicResource dr = DynamicResource.INSTANCE.get(context);
+        ResourceProvider plugin = dr.mPlugin;
+        return plugin == null ? dr : plugin;
+    }
+}
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 4d5ee49..0a32734 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -19,7 +19,6 @@
 import android.os.Looper;
 import android.os.Process;
 
-import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -36,9 +35,9 @@
     private static final int KEEP_ALIVE = 1;
 
     /**
-     * An {@link Executor} to be used with async task with no limit on the queue size.
+     * An {@link ThreadPoolExecutor} to be used with async task with no limit on the queue size.
      */
-    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
+    public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
             TimeUnit.SECONDS, new LinkedBlockingQueue<>());
 
diff --git a/src/com/android/launcher3/util/GridOccupancy.java b/src/com/android/launcher3/util/GridOccupancy.java
index 6a10b0a..9c752a7 100644
--- a/src/com/android/launcher3/util/GridOccupancy.java
+++ b/src/com/android/launcher3/util/GridOccupancy.java
@@ -2,7 +2,7 @@
 
 import android.graphics.Rect;
 
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.model.data.ItemInfo;
 
 /**
  * Utility object to manage the occupancy in a grid.
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
index 4a4a5ca..fcb96d7 100644
--- a/src/com/android/launcher3/util/IOUtils.java
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.util.Log;
 
-import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 
 import java.io.ByteArrayOutputStream;
@@ -67,7 +66,7 @@
      * Utility method to debug binary data
      */
     public static String createTempFile(Context context, byte[] data) {
-        if (!FeatureFlags.IS_DOGFOOD_BUILD) {
+        if (!FeatureFlags.IS_STUDIO_BUILD) {
             throw new IllegalStateException("Method only allowed in development mode");
         }
 
@@ -87,7 +86,7 @@
             try {
                 c.close();
             } catch (IOException e) {
-                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                if (FeatureFlags.IS_STUDIO_BUILD) {
                     Log.d(TAG, "Error closing", e);
                 }
             }
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index 031a40d..6f706d2 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -21,8 +21,8 @@
 import android.content.pm.PackageManager;
 import android.util.Log;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.R;
+import com.android.launcher3.model.data.AppInfo;
 
 /**
  * A wrapper class to access instant app related APIs.
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 59a5ed6..4d5405d 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -19,11 +19,11 @@
 import android.content.ComponentName;
 import android.os.UserHandle;
 
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
+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.shortcuts.ShortcutKey;
 
 import java.util.HashSet;
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 4acdb5c..528a6e9 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -12,6 +12,8 @@
 public class LogConfig {
     // These are list of strings that can be used to replace TAGNAME.
 
+    public static final String STATSLOG = "StatsLog";
+
     /**
      * After this tag is turned on, whenever there is n user event, debug information is
      * printed out to logcat.
@@ -28,4 +30,9 @@
      * When turned on, icon cache is only fetched from memory and not disk.
      */
     public static final String MEMORY_ONLY_ICON_CACHE = "MemoryOnlyIconCache";
+
+    /**
+     * When turned on, we enable doodle related logging.
+     */
+    public static final String DOODLE_LOGGING = "DoodleLogging";
 }
diff --git a/src/com/android/launcher3/util/LooperExecutor.java b/src/com/android/launcher3/util/LooperExecutor.java
index 8ac600f..3a8a13c 100644
--- a/src/com/android/launcher3/util/LooperExecutor.java
+++ b/src/com/android/launcher3/util/LooperExecutor.java
@@ -41,10 +41,10 @@
 
     @Override
     public void execute(Runnable runnable) {
-        if (mHandler.getLooper() == Looper.myLooper()) {
+        if (getHandler().getLooper() == Looper.myLooper()) {
             runnable.run();
         } else {
-            mHandler.post(runnable);
+            getHandler().post(runnable);
         }
     }
 
@@ -52,7 +52,7 @@
      * Same as execute, but never runs the action inline.
      */
     public void post(Runnable runnable) {
-        mHandler.post(runnable);
+        getHandler().post(runnable);
     }
 
     /**
@@ -96,14 +96,14 @@
      * Returns the thread for this executor
      */
     public Thread getThread() {
-        return mHandler.getLooper().getThread();
+        return getHandler().getLooper().getThread();
     }
 
     /**
      * Returns the looper for this executor
      */
     public Looper getLooper() {
-        return mHandler.getLooper();
+        return getHandler().getLooper();
     }
 
     /**
diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java
index 2896535..f4ccf42 100644
--- a/src/com/android/launcher3/util/LooperIdleLock.java
+++ b/src/com/android/launcher3/util/LooperIdleLock.java
@@ -22,29 +22,30 @@
 /**
  * Utility class to block execution until the UI looper is idle.
  */
-public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {
+public class LooperIdleLock implements MessageQueue.IdleHandler {
 
     private final Object mLock;
 
     private boolean mIsLocked;
+    private Looper mLooper;
 
     public LooperIdleLock(Object lock, Looper looper) {
         mLock = lock;
+        mLooper = looper;
         mIsLocked = true;
         looper.getQueue().addIdleHandler(this);
     }
 
     @Override
-    public void run() {
-        Looper.myQueue().addIdleHandler(this);
-    }
-
-    @Override
     public boolean queueIdle() {
         synchronized (mLock) {
             mIsLocked = false;
             mLock.notify();
         }
+        // Manually remove from the list in case we're calling this outside of the idle callbacks
+        // (this is Ok in the normal flow as well because MessageQueue makes a copy of all handlers
+        // before calling back)
+        mLooper.getQueue().removeIdleHandler(this);
         return false;
     }
 
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index fe9c2c4..fc9f8f7 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
 
 import java.util.concurrent.ExecutionException;
@@ -39,9 +40,14 @@
     }
 
     public T get(Context context) {
+        if (context instanceof PreviewContext) {
+            return ((PreviewContext) context).getObject(this, mProvider);
+        }
+
         if (mValue == null) {
             if (Looper.myLooper() == Looper.getMainLooper()) {
-                mValue = mProvider.get(context.getApplicationContext());
+                mValue = TraceHelper.whitelistIpcs("main.thread.object",
+                        () -> mProvider.get(context.getApplicationContext()));
             } else {
                 try {
                     return MAIN_EXECUTOR.submit(() -> get(context)).get();
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 07f835d..a8642b0 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.util;
 
-import android.util.Property;
+import android.util.FloatProperty;
 import android.view.View;
 
 import java.util.Arrays;
@@ -26,8 +26,8 @@
  */
 public class MultiValueAlpha {
 
-    public static final Property<AlphaProperty, Float> VALUE =
-            new Property<AlphaProperty, Float>(Float.TYPE, "value") {
+    public static final FloatProperty<AlphaProperty> VALUE =
+            new FloatProperty<AlphaProperty>("value") {
 
                 @Override
                 public Float get(AlphaProperty alphaProperty) {
@@ -35,7 +35,7 @@
                 }
 
                 @Override
-                public void set(AlphaProperty object, Float value) {
+                public void setValue(AlphaProperty object, float value) {
                     object.setValue(value);
                 }
             };
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java b/src/com/android/launcher3/util/ObjectWrapper.java
similarity index 90%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
rename to src/com/android/launcher3/util/ObjectWrapper.java
index abfe3ad..e5b4707 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
+++ b/src/com/android/launcher3/util/ObjectWrapper.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.quickstep.util;
+package com.android.launcher3.util;
 
 import android.os.Binder;
 import android.os.IBinder;
@@ -25,7 +25,7 @@
  */
 public class ObjectWrapper<T> extends Binder {
 
-    private final T mObject;
+    private T mObject;
 
     public ObjectWrapper(T object) {
         mObject = object;
@@ -35,6 +35,10 @@
         return mObject;
     }
 
+    public void clear() {
+        mObject = null;
+    }
+
     public static IBinder wrap(Object obj) {
         return new ObjectWrapper<>(obj);
     }
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
new file mode 100644
index 0000000..d4e074c
--- /dev/null
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.SharedPreferences;
+import android.util.ArrayMap;
+
+import androidx.annotation.StringDef;
+
+import com.android.launcher3.Launcher;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Stores and retrieves onboarding-related data via SharedPreferences.
+ */
+public class OnboardingPrefs<T extends Launcher> {
+
+    public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
+    public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
+    public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
+    public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
+    public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
+    public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
+    public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
+
+    /**
+     * Events that either have happened or have not (booleans).
+     */
+    @StringDef(value = {
+            HOME_BOUNCE_SEEN,
+            SHELF_BOUNCE_SEEN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventBoolKey {}
+
+    /**
+     * Events that occur multiple times, which we count up to a max defined in {@link #MAX_COUNTS}.
+     */
+    @StringDef(value = {
+            HOME_BOUNCE_COUNT,
+            SHELF_BOUNCE_COUNT,
+            ALL_APPS_COUNT,
+            HOTSEAT_DISCOVERY_TIP_COUNT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventCountKey {}
+
+    private static final Map<String, Integer> MAX_COUNTS;
+    static {
+        Map<String, Integer> maxCounts = new ArrayMap<>(3);
+        maxCounts.put(HOME_BOUNCE_COUNT, 3);
+        maxCounts.put(SHELF_BOUNCE_COUNT, 3);
+        maxCounts.put(ALL_APPS_COUNT, 5);
+        maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
+        MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
+    }
+
+    protected final T mLauncher;
+    protected final SharedPreferences mSharedPrefs;
+
+    public OnboardingPrefs(T launcher, SharedPreferences sharedPrefs) {
+        mLauncher = launcher;
+        mSharedPrefs = sharedPrefs;
+    }
+
+    /** @return The number of times we have seen the given event. */
+    public int getCount(@EventCountKey String key) {
+        return mSharedPrefs.getInt(key, 0);
+    }
+
+    /** @return Whether we have seen this event enough times, as defined by {@link #MAX_COUNTS}. */
+    public boolean hasReachedMaxCount(@EventCountKey String eventKey) {
+        return hasReachedMaxCount(getCount(eventKey), eventKey);
+    }
+
+    private boolean hasReachedMaxCount(int count, @EventCountKey String eventKey) {
+        return count >= MAX_COUNTS.get(eventKey);
+    }
+
+    /** @return Whether we have seen the given event. */
+    public boolean getBoolean(@EventBoolKey String key) {
+        return mSharedPrefs.getBoolean(key, false);
+    }
+
+    /**
+     * Marks on-boarding preference boolean at true
+     */
+    public void markChecked(String flag) {
+        mSharedPrefs.edit().putBoolean(flag, true).apply();
+    }
+
+    /**
+     * Add 1 to the given event count, if we haven't already reached the max count.
+     * @return Whether we have now reached the max count.
+     */
+    public boolean incrementEventCount(@EventCountKey String eventKey) {
+        int count = getCount(eventKey);
+        if (hasReachedMaxCount(count, eventKey)) {
+            return true;
+        }
+        count++;
+        mSharedPrefs.edit().putInt(eventKey, count).apply();
+        return hasReachedMaxCount(count, eventKey);
+    }
+}
diff --git a/src/com/android/launcher3/util/OverScroller.java b/src/com/android/launcher3/util/OverScroller.java
index fc8a138..87e6986 100644
--- a/src/com/android/launcher3/util/OverScroller.java
+++ b/src/com/android/launcher3/util/OverScroller.java
@@ -26,11 +26,13 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 
-import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.launcher3.R;
+import com.android.systemui.plugins.ResourceProvider;
+
 /**
  * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more
  * customization options.
@@ -163,6 +165,9 @@
     /**
      * Returns how long the scroll event will take, in milliseconds.
      *
+     * Note that if mScroller.mState == SPRING, this duration is ignored, so can only
+     * serve as an estimate for how long the spring-controlled scroll will take.
+     *
      * @return The duration of the scroll in milliseconds.
      */
     public final int getDuration() {
@@ -425,6 +430,7 @@
         // Current state of the animation.
         private int mState = SPLINE;
 
+        private Context mContext;
         private SpringAnimation mSpring;
 
         // Constant gravity value, used in the deceleration phase.
@@ -500,6 +506,7 @@
         }
 
         SplineOverScroller(Context context) {
+            mContext = context;
             mFinished = true;
             final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
             mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
@@ -554,21 +561,25 @@
             mStartTime = AnimationUtils.currentAnimationTimeMillis();
             mDuration = duration;
 
+            if (mSpring != null) {
+                mSpring.cancel();
+            }
+
             if (mState == SPRING) {
-                if (mSpring != null) {
-                    mSpring.cancel();
-                }
                 mSpring = new SpringAnimation(this, SPRING_PROPERTY);
 
+                ResourceProvider rp = DynamicResource.provider(mContext);
+                float stiffness = rp.getFloat(R.dimen.horizontal_spring_stiffness);
+                float damping = rp.getFloat(R.dimen.horizontal_spring_damping_ratio);
                 mSpring.setSpring(new SpringForce(mFinal)
-                        .setStiffness(SpringForce.STIFFNESS_LOW)
-                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+                        .setStiffness(stiffness)
+                        .setDampingRatio(damping));
                 mSpring.setStartVelocity(velocity);
                 mSpring.animateToFinalPosition(mFinal);
                 mSpring.addEndListener((animation, canceled, value, velocity1) -> {
+                    mSpring = null;
                     finish();
                     mState = SPLINE;
-                    mSpring = null;
                 });
             }
             // Unused
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index e97adb5..86f3431 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
 import android.app.AppOpsManager;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -24,6 +26,7 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -34,20 +37,21 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PatternMatcher;
+import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 import android.widget.Toast;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.Utilities;
+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.PromiseAppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 import java.net.URISyntaxException;
 import java.util.List;
@@ -61,12 +65,12 @@
 
     private final Context mContext;
     private final PackageManager mPm;
-    private final LauncherAppsCompat mLauncherApps;
+    private final LauncherApps mLauncherApps;
 
     public PackageManagerHelper(Context context) {
         mContext = context;
         mPm = context.getPackageManager();
-        mLauncherApps = LauncherAppsCompat.getInstance(context);
+        mLauncherApps = context.getSystemService(LauncherApps.class);
     }
 
     /**
@@ -74,8 +78,8 @@
      * guarantee that the app is on SD card.
      */
     public boolean isAppOnSdcard(String packageName, UserHandle user) {
-        ApplicationInfo info = mLauncherApps.getApplicationInfo(
-                packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, user);
+        ApplicationInfo info = getApplicationInfo(
+                packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES);
         return info != null && (info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
     }
 
@@ -84,10 +88,47 @@
      * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}.
      */
     public boolean isAppSuspended(String packageName, UserHandle user) {
-        ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, 0, user);
+        ApplicationInfo info = getApplicationInfo(packageName, user, 0);
         return info != null && isAppSuspended(info);
     }
 
+    /**
+     * Returns the application info for the provided package or null
+     */
+    public ApplicationInfo getApplicationInfo(String packageName, UserHandle user, int flags) {
+        if (Utilities.ATLEAST_OREO) {
+            try {
+                ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
+                return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
+                        ? null : info;
+            } catch (PackageManager.NameNotFoundException e) {
+                return null;
+            }
+        } else {
+            final boolean isPrimaryUser = Process.myUserHandle().equals(user);
+            if (!isPrimaryUser && (flags == 0)) {
+                // We are looking for an installed app on a secondary profile. Prior to O, the only
+                // entry point for work profiles is through the LauncherActivity.
+                List<LauncherActivityInfo> activityList =
+                        mLauncherApps.getActivityList(packageName, user);
+                return activityList.size() > 0 ? activityList.get(0).getApplicationInfo() : null;
+            }
+            try {
+                ApplicationInfo info = mPm.getApplicationInfo(packageName, flags);
+                // There is no way to check if the app is installed for managed profile. But for
+                // primary profile, we can still have this check.
+                if (isPrimaryUser && ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0)
+                        || !info.enabled) {
+                    return null;
+                }
+                return info;
+            } catch (PackageManager.NameNotFoundException e) {
+                // Package not found
+                return null;
+            }
+        }
+    }
+
     public boolean isSafeMode() {
         return mContext.getPackageManager().isSafeMode();
     }
@@ -202,8 +243,7 @@
         }
         if (componentName != null) {
             try {
-                mLauncherApps.showAppDetailsForProfile(
-                        componentName, info.user, sourceBounds, opts);
+                mLauncherApps.startAppDetailsActivity(componentName, info.user, sourceBounds, opts);
             } catch (SecurityException | ActivityNotFoundException e) {
                 Toast.makeText(mContext, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
                 Log.e(TAG, "Unable to launch settings", e);
@@ -259,16 +299,13 @@
      */
     public static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
         final Intent intent = new Intent(action);
-        for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
-            if (info.activityInfo != null &&
-                    (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
-                final String packageName = info.activityInfo.packageName;
-                try {
-                    final Resources res = pm.getResourcesForApplication(packageName);
-                    return Pair.create(packageName, res);
-                } catch (NameNotFoundException e) {
-                    Log.w(TAG, "Failed to find resources for " + packageName);
-                }
+        for (ResolveInfo info : pm.queryBroadcastReceivers(intent, MATCH_SYSTEM_ONLY)) {
+            final String packageName = info.activityInfo.packageName;
+            try {
+                final Resources res = pm.getResourcesForApplication(packageName);
+                return Pair.create(packageName, res);
+            } catch (NameNotFoundException e) {
+                Log.w(TAG, "Failed to find resources for " + packageName);
             }
         }
         return null;
@@ -295,4 +332,17 @@
         }
         return false;
     }
+
+    /**
+     * Returns true if Launcher has the permission to access shortcuts.
+     * @see LauncherApps#hasShortcutHostPermission()
+     */
+    public static boolean hasShortcutsPermission(Context context) {
+        try {
+            return context.getSystemService(LauncherApps.class).hasShortcutHostPermission();
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to make shortcut manager call", e);
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/util/PackageUserKey.java b/src/com/android/launcher3/util/PackageUserKey.java
index f243ca6..3a3b5a2 100644
--- a/src/com/android/launcher3/util/PackageUserKey.java
+++ b/src/com/android/launcher3/util/PackageUserKey.java
@@ -3,9 +3,10 @@
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.model.data.ItemInfo;
 
 import java.util.Arrays;
 
@@ -60,4 +61,10 @@
         PackageUserKey otherKey = (PackageUserKey) obj;
         return mPackageName.equals(otherKey.mPackageName) && mUser.equals(otherKey.mUser);
     }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return mPackageName + "#" + mUser;
+    }
 }
diff --git a/src/com/android/launcher3/util/PendingAnimation.java b/src/com/android/launcher3/util/PendingAnimation.java
deleted file mode 100644
index 617a38b..0000000
--- a/src/com/android/launcher3/util/PendingAnimation.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util;
-
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Utility class to keep track of a running animation.
- *
- * This class allows attaching end callbacks to an animation is intended to be used with
- * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case
- * AnimationListeners are not properly dispatched.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class PendingAnimation {
-
-    private final ArrayList<Consumer<OnEndListener>> mEndListeners = new ArrayList<>();
-
-    public final AnimatorSet anim;
-
-    public PendingAnimation(AnimatorSet anim) {
-        this.anim = anim;
-    }
-
-    public void finish(boolean isSuccess, int logAction) {
-        for (Consumer<OnEndListener> listeners : mEndListeners) {
-            listeners.accept(new OnEndListener(isSuccess, logAction));
-        }
-        mEndListeners.clear();
-    }
-
-    public void addEndListener(Consumer<OnEndListener> listener) {
-        mEndListeners.add(listener);
-    }
-
-    public static class OnEndListener {
-        public boolean isSuccess;
-        public int logAction;
-
-        public OnEndListener(boolean isSuccess, int logAction) {
-            this.isSuccess = isSuccess;
-            this.logAction = logAction;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/util/PendingRequestArgs.java b/src/com/android/launcher3/util/PendingRequestArgs.java
index b8bcfed..9b8c6a6 100644
--- a/src/com/android/launcher3/util/PendingRequestArgs.java
+++ b/src/com/android/launcher3/util/PendingRequestArgs.java
@@ -20,7 +20,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 
 /**
diff --git a/src/com/android/launcher3/util/Preconditions.java b/src/com/android/launcher3/util/Preconditions.java
index ed66422..63d56e4 100644
--- a/src/com/android/launcher3/util/Preconditions.java
+++ b/src/com/android/launcher3/util/Preconditions.java
@@ -28,25 +28,25 @@
 public class Preconditions {
 
     public static void assertNotNull(Object o) {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && o == null) {
+        if (FeatureFlags.IS_STUDIO_BUILD && o == null) {
             throw new IllegalStateException();
         }
     }
 
     public static void assertWorkerThread() {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(MODEL_EXECUTOR.getLooper())) {
+        if (FeatureFlags.IS_STUDIO_BUILD && !isSameLooper(MODEL_EXECUTOR.getLooper())) {
             throw new IllegalStateException();
         }
     }
 
     public static void assertUIThread() {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(Looper.getMainLooper())) {
+        if (FeatureFlags.IS_STUDIO_BUILD && !isSameLooper(Looper.getMainLooper())) {
             throw new IllegalStateException();
         }
     }
 
     public static void assertNonUiThread() {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && isSameLooper(Looper.getMainLooper())) {
+        if (FeatureFlags.IS_STUDIO_BUILD && isSameLooper(Looper.getMainLooper())) {
             throw new IllegalStateException();
         }
     }
diff --git a/src/com/android/launcher3/util/RaceConditionTracker.java b/src/com/android/launcher3/util/RaceConditionTracker.java
deleted file mode 100644
index 6954d0e..0000000
--- a/src/com/android/launcher3/util/RaceConditionTracker.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-/**
- * Event tracker for reliably reproducing race conditions in tests.
- * The app should call onEvent() for events that the test will try to reproduce in all possible
- * orders.
- */
-public class RaceConditionTracker {
-    public final static boolean ENTER = true;
-    public final static boolean EXIT = false;
-    static final String ENTER_POSTFIX = "enter";
-    static final String EXIT_POSTFIX = "exit";
-
-    public interface EventProcessor {
-        void onEvent(String eventName);
-    }
-
-    private static EventProcessor sEventProcessor;
-
-    static void setEventProcessor(EventProcessor eventProcessor) {
-        sEventProcessor = eventProcessor;
-    }
-
-    public static void onEvent(String eventName) {
-        if (sEventProcessor != null) sEventProcessor.onEvent(eventName);
-    }
-
-    public static void onEvent(String eventName, boolean isEnter) {
-        if (sEventProcessor != null) {
-            sEventProcessor.onEvent(enterExitEvt(eventName, isEnter));
-        }
-    }
-
-    public static String enterExitEvt(String eventName, boolean isEnter) {
-        return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX);
-    }
-
-    public static String enterEvt(String eventName) {
-        return enterExitEvt(eventName, ENTER);
-    }
-
-    public static String exitEvt(String eventName) {
-        return enterExitEvt(eventName, EXIT);
-    }
-}
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index af99713..91cf835 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -15,11 +15,11 @@
  */
 package com.android.launcher3.util;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 
 public class ShortcutUtil {
@@ -34,7 +34,7 @@
      * Returns true when we should show depp shortcuts in shortcut menu for the item.
      */
     public static boolean supportsDeepShortcuts(ItemInfo info) {
-        return isActive(info) && isApp(info);
+        return isActive(info) && isApp(info) && !WidgetsModel.GO_DISABLE_WIDGETS;
     }
 
     /**
@@ -64,7 +64,7 @@
     private static boolean isActive(ItemInfo info) {
         boolean isLoading = info instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) info).hasPromiseIconUi();
-        return !isLoading && !info.isDisabled() && !FeatureFlags.GO_DISABLE_WIDGETS;
+        return !isLoading && !info.isDisabled();
     }
 
     private static boolean isApp(ItemInfo info) {
@@ -76,4 +76,4 @@
                 && info.container != ItemInfo.NO_ID
                 && info instanceof WorkspaceItemInfo;
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
new file mode 100644
index 0000000..465a0e8
--- /dev/null
+++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import java.util.function.Consumer;
+
+public class SimpleBroadcastReceiver extends BroadcastReceiver {
+
+    private final Consumer<Intent> mIntentConsumer;
+
+    public SimpleBroadcastReceiver(Consumer<Intent> intentConsumer) {
+        mIntentConsumer = intentConsumer;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        mIntentConsumer.accept(intent);
+    }
+
+    /**
+     * Helper method to register multiple actions
+     */
+    public void register(Context context, String... actions) {
+        IntentFilter filter = new IntentFilter();
+        for (String action : actions) {
+            filter.addAction(action);
+        }
+        context.registerReceiver(this, filter);
+    }
+}
diff --git a/src/com/android/launcher3/util/SystemUiController.java b/src/com/android/launcher3/util/SystemUiController.java
index 86995b7..275c024 100644
--- a/src/com/android/launcher3/util/SystemUiController.java
+++ b/src/com/android/launcher3/util/SystemUiController.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.util;
 
-import android.text.TextUtils;
 import android.view.View;
 import android.view.Window;
 
@@ -31,10 +30,9 @@
 
     // Various UI states in increasing order of priority
     public static final int UI_STATE_BASE_WINDOW = 0;
-    public static final int UI_STATE_ALL_APPS = 1;
+    public static final int UI_STATE_SCRIM_VIEW = 1;
     public static final int UI_STATE_WIDGET_BOTTOM_SHEET = 2;
-    public static final int UI_STATE_ROOT_VIEW = 3;
-    public static final int UI_STATE_OVERVIEW = 4;
+    public static final int UI_STATE_OVERVIEW = 3;
 
     public static final int FLAG_LIGHT_NAV = 1 << 0;
     public static final int FLAG_DARK_NAV = 1 << 1;
@@ -42,7 +40,7 @@
     public static final int FLAG_DARK_STATUS = 1 << 3;
 
     private final Window mWindow;
-    private final int[] mStates = new int[5];
+    private final int[] mStates = new int[4];
 
     public SystemUiController(Window window) {
         mWindow = window;
@@ -63,25 +61,38 @@
         // Apply the state flags in priority order
         int newFlags = oldFlags;
         for (int stateFlag : mStates) {
-            if (Utilities.ATLEAST_OREO) {
-                if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
-                    newFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
-                } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
-                    newFlags &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
-                }
-            }
-
-            if ((stateFlag & FLAG_LIGHT_STATUS) != 0) {
-                newFlags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
-            } else if ((stateFlag & FLAG_DARK_STATUS) != 0) {
-                newFlags &= ~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
-            }
+            newFlags = getSysUiVisibilityFlags(stateFlag, newFlags);
         }
         if (newFlags != oldFlags) {
             mWindow.getDecorView().setSystemUiVisibility(newFlags);
         }
     }
 
+    /**
+     * Returns the sysui visibility for the base layer
+     */
+    public int getBaseSysuiVisibility() {
+        return getSysUiVisibilityFlags(
+                mStates[UI_STATE_BASE_WINDOW], mWindow.getDecorView().getSystemUiVisibility());
+    }
+
+    private int getSysUiVisibilityFlags(int stateFlag, int currentVisibility) {
+        if (Utilities.ATLEAST_OREO) {
+            if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
+                currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+            } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
+                currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+            }
+        }
+
+        if ((stateFlag & FLAG_LIGHT_STATUS) != 0) {
+            currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+        } else if ((stateFlag & FLAG_DARK_STATUS) != 0) {
+            currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+        }
+        return currentVisibility;
+    }
+
     @Override
     public String toString() {
         return "mStates=" + Arrays.toString(mStates);
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index 0d02715..b74686f 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -36,7 +36,7 @@
 public class Themes {
 
     public static int getActivityThemeRes(Context context) {
-        WallpaperColorInfo wallpaperColorInfo = WallpaperColorInfo.getInstance(context);
+        WallpaperColorInfo wallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
         boolean darkTheme;
         if (Utilities.ATLEAST_Q) {
             Configuration configuration = context.getResources().getConfiguration();
@@ -113,10 +113,17 @@
      * Returns the alpha corresponding to the theme attribute {@param attr}, in the range [0, 255].
      */
     public static int getAlpha(Context context, int attr) {
+        return (int) (255 * getFloat(context, attr, 0) + 0.5f);
+    }
+
+    /**
+     * Returns the alpha corresponding to the theme attribute {@param attr}
+     */
+    public static float getFloat(Context context, int attr, float defValue) {
         TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
-        float alpha = ta.getFloat(0, 0);
+        float value = ta.getFloat(0, defValue);
         ta.recycle();
-        return (int) (255 * alpha + 0.5f);
+        return value;
     }
 
     /**
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index c24bb67..168227d 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -15,19 +15,14 @@
  */
 package com.android.launcher3.util;
 
-import static android.util.Log.VERBOSE;
-import static android.util.Log.isLoggable;
-
-import android.os.SystemClock;
 import android.os.Trace;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.MutableLong;
 
-import com.android.launcher3.config.FeatureFlags;
+import androidx.annotation.MainThread;
+
+import java.util.function.Supplier;
 
 /**
- * A wrapper around {@link Trace} with some utility information.
+ * A wrapper around {@link Trace} to allow better testing.
  *
  * To enable any tracing log, execute the following command:
  * $ adb shell setprop log.tag.LAUNCHER_TRACE VERBOSE
@@ -35,65 +30,60 @@
  */
 public class TraceHelper {
 
-    private static final boolean ENABLED = isLoggable("LAUNCHER_TRACE", VERBOSE);
+    // Track binder class for this trace
+    public static final int FLAG_ALLOW_BINDER_TRACKING = 1 << 0;
 
-    private static final boolean SYSTEM_TRACE = ENABLED;
-    private static final ArrayMap<String, MutableLong> sUpTimes = ENABLED ? new ArrayMap<>() : null;
+    // Temporarily ignore blocking binder calls for this trace.
+    public static final int FLAG_IGNORE_BINDERS = 1 << 1;
 
-    public static void beginSection(String sectionName) {
-        if (ENABLED) {
-            synchronized (sUpTimes) {
-                MutableLong time = sUpTimes.get(sectionName);
-                if (time == null) {
-                    time = new MutableLong(isLoggable(sectionName, VERBOSE) ? 0 : -1);
-                    sUpTimes.put(sectionName, time);
-                }
-                if (time.value >= 0) {
-                    if (SYSTEM_TRACE) {
-                        Trace.beginSection(sectionName);
-                    }
-                    time.value = SystemClock.uptimeMillis();
-                }
-            }
-        }
+    public static final int FLAG_CHECK_FOR_RACE_CONDITIONS = 1 << 2;
+
+    public static final int FLAG_UI_EVENT =
+            FLAG_ALLOW_BINDER_TRACKING | FLAG_CHECK_FOR_RACE_CONDITIONS;
+
+    /**
+     * Static instance of Trace helper, overridden in tests.
+     */
+    public static TraceHelper INSTANCE = new TraceHelper();
+
+    /**
+     * @return a token to pass into {@link #endSection(Object)}.
+     */
+    public Object beginSection(String sectionName) {
+        return beginSection(sectionName, 0);
     }
 
-    public static void partitionSection(String sectionName, String partition) {
-        if (ENABLED) {
-            synchronized (sUpTimes) {
-                MutableLong time = sUpTimes.get(sectionName);
-                if (time != null && time.value >= 0) {
-
-                    if (SYSTEM_TRACE) {
-                        Trace.endSection();
-                        Trace.beginSection(sectionName);
-                    }
-
-                    long now = SystemClock.uptimeMillis();
-                    Log.d(sectionName, partition + " : " + (now - time.value));
-                    time.value = now;
-                }
-            }
-        }
+    public Object beginSection(String sectionName, int flags) {
+        Trace.beginSection(sectionName);
+        return null;
     }
 
-    public static void endSection(String sectionName) {
-        if (ENABLED) {
-            endSection(sectionName, "End");
-        }
+    /**
+     * @param token the token returned from {@link #beginSection(String, int)}
+     */
+    public void endSection(Object token) {
+        Trace.endSection();
     }
 
-    public static void endSection(String sectionName, String msg) {
-        if (ENABLED) {
-            synchronized (sUpTimes) {
-                MutableLong time = sUpTimes.get(sectionName);
-                if (time != null && time.value >= 0) {
-                    if (SYSTEM_TRACE) {
-                        Trace.endSection();
-                    }
-                    Log.d(sectionName, msg + " : " + (SystemClock.uptimeMillis() - time.value));
-                }
-            }
+    /**
+     * Similar to {@link #beginSection} but doesn't add a trace section.
+     */
+    public Object beginFlagsOverride(int flags) {
+        return null;
+    }
+
+    public void endFlagsOverride(Object token) { }
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+     */
+    @MainThread
+    public static <T> T whitelistIpcs(String rpcName, Supplier<T> supplier) {
+        Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
+        try {
+            return supplier.get();
+        } finally {
+            INSTANCE.endSection(traceToken);
         }
     }
 }
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index f8d1632..0498052 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -29,38 +29,39 @@
  */
 public class UiThreadHelper {
 
-    private static Handler sHandler;
+    private static final MainThreadInitializedObject<Handler> HANDLER =
+            new MainThreadInitializedObject<>(
+                    c -> new Handler(UI_HELPER_EXECUTOR.getLooper(), new UiCallbacks(c)));
 
     private static final int MSG_HIDE_KEYBOARD = 1;
     private static final int MSG_SET_ORIENTATION = 2;
     private static final int MSG_RUN_COMMAND = 3;
 
-    private static Handler getHandler(Context context) {
-        if (sHandler == null) {
-            sHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(),
-                    new UiCallbacks(context.getApplicationContext()));
-        }
-        return sHandler;
-    }
-
     public static void hideKeyboardAsync(Context context, IBinder token) {
-        Message.obtain(getHandler(context), MSG_HIDE_KEYBOARD, token).sendToTarget();
+        Message.obtain(HANDLER.get(context), MSG_HIDE_KEYBOARD, token).sendToTarget();
     }
 
     public static void setOrientationAsync(Activity activity, int orientation) {
-        Message.obtain(getHandler(activity), MSG_SET_ORIENTATION, orientation, 0, activity)
+        Message.obtain(HANDLER.get(activity), MSG_SET_ORIENTATION, orientation, 0, activity)
                 .sendToTarget();
     }
 
+    public static void setBackButtonAlphaAsync(Context context, AsyncCommand command, float alpha,
+            boolean animate) {
+        runAsyncCommand(context, command, Float.floatToIntBits(alpha), animate ? 1 : 0);
+    }
+
     public static void runAsyncCommand(Context context, AsyncCommand command, int arg1, int arg2) {
-        Message.obtain(getHandler(context), MSG_RUN_COMMAND, arg1, arg2, command).sendToTarget();
+        Message.obtain(HANDLER.get(context), MSG_RUN_COMMAND, arg1, arg2, command).sendToTarget();
     }
 
     private static class UiCallbacks implements Handler.Callback {
 
+        private final Context mContext;
         private final InputMethodManager mIMM;
 
         UiCallbacks(Context context) {
+            mContext = context;
             mIMM = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
         }
 
@@ -74,7 +75,7 @@
                     ((Activity) message.obj).setRequestedOrientation(message.arg1);
                     return true;
                 case MSG_RUN_COMMAND:
-                    ((AsyncCommand) message.obj).execute(message.arg1, message.arg2);
+                    ((AsyncCommand) message.obj).execute(mContext, message.arg1, message.arg2);
                     return true;
             }
             return false;
@@ -82,7 +83,6 @@
     }
 
     public interface AsyncCommand {
-
-        void execute(int arg1, int arg2);
+        void execute(Context proxy, int arg1, int arg2);
     }
 }
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 04741a1..b0defd4 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -39,7 +39,7 @@
     public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
             new MainThreadInitializedObject<>(VibratorWrapper::new);
 
-    private static final VibrationEffect EFFECT_CLICK =
+    public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
     /**
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 5a131c8..82e24c2 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -23,10 +23,13 @@
 import android.view.View.OnAttachStateChangeListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Launcher;
 
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * An executor which runs all the tasks after the first onDraw is called on the target view.
@@ -36,7 +39,7 @@
 
     private final ArrayList<Runnable> mTasks = new ArrayList<>();
 
-    private Launcher mLauncher;
+    private Consumer<ViewOnDrawExecutor> mOnClearCallback;
     private View mAttachedView;
     private boolean mCompleted;
 
@@ -44,11 +47,16 @@
     private boolean mFirstDrawCompleted;
 
     public void attachTo(Launcher launcher) {
-        attachTo(launcher, launcher.getWorkspace(), true /* waitForLoadAnimation */);
+        attachTo(launcher.getWorkspace(), true /* waitForLoadAnimation */,
+                launcher::clearPendingExecutor);
     }
 
-    public void attachTo(Launcher launcher, View attachedView, boolean waitForLoadAnimation) {
-        mLauncher = launcher;
+    /**
+     * Attached the executor to the existence of the view
+     */
+    public void attachTo(View attachedView, boolean waitForLoadAnimation,
+            Consumer<ViewOnDrawExecutor> onClearCallback) {
+        mOnClearCallback = onClearCallback;
         mAttachedView = attachedView;
         mAttachedView.addOnAttachStateChangeListener(this);
         if (!waitForLoadAnimation) {
@@ -108,8 +116,8 @@
             mAttachedView.getViewTreeObserver().removeOnDrawListener(this);
             mAttachedView.removeOnAttachStateChangeListener(this);
         }
-        if (mLauncher != null) {
-            mLauncher.clearPendingExecutor(this);
+        if (mOnClearCallback != null) {
+            mOnClearCallback.accept(this);
         }
         MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
     }
@@ -118,7 +126,11 @@
         return mCompleted;
     }
 
-    protected void runAllTasks() {
+    /**
+     * Executes all tasks immediately
+     */
+    @VisibleForTesting
+    public void runAllTasks() {
         for (final Runnable r : mTasks) {
             r.run();
         }
diff --git a/src/com/android/launcher3/util/WindowBounds.java b/src/com/android/launcher3/util/WindowBounds.java
new file mode 100644
index 0000000..3c2fb62
--- /dev/null
+++ b/src/com/android/launcher3/util/WindowBounds.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility class to hold information about window position and layout
+ */
+public class WindowBounds {
+
+    public final Rect bounds;
+    public final Rect insets;
+    public final Point availableSize;
+
+    public WindowBounds(Rect bounds, Rect insets) {
+        this.bounds = bounds;
+        this.insets = insets;
+        availableSize = new Point(bounds.width() - insets.left - insets.right,
+                bounds.height() - insets.top - insets.bottom);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof WindowBounds)) {
+            return false;
+        }
+        WindowBounds other = (WindowBounds) obj;
+        return other.bounds.equals(bounds) && other.insets.equals(insets);
+    }
+}
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 195a77a..11c1029 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.views;
 
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
 import android.animation.Animator;
@@ -41,7 +43,7 @@
 public abstract class AbstractSlideInView extends AbstractFloatingView
         implements SingleAxisSwipeDetector.Listener {
 
-    protected static Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
+    protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
 
                 @Override
@@ -62,6 +64,7 @@
     protected final ObjectAnimator mOpenCloseAnimator;
 
     protected View mContent;
+    private final View mColorScrim;
     protected Interpolator mScrollInterpolator;
 
     // range [0, 1], 0=> completely open, 1=> completely closed
@@ -85,11 +88,30 @@
                 announceAccessibilityChanges();
             }
         });
+        int scrimColor = getScrimColor(mLauncher);
+        mColorScrim = scrimColor != -1 ? createColorScrim(mLauncher, scrimColor) : null;
+    }
+
+    protected void attachToContainer() {
+        if (mColorScrim != null) {
+            getPopupContainer().addView(mColorScrim);
+        }
+        getPopupContainer().addView(this);
+    }
+
+    /**
+     * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added.
+     */
+    protected int getScrimColor(Context context) {
+        return -1;
     }
 
     protected void setTranslationShift(float translationShift) {
         mTranslationShift = translationShift;
         mContent.setTranslationY(mTranslationShift * mContent.getHeight());
+        if (mColorScrim != null) {
+            mColorScrim.setAlpha(1 - mTranslationShift);
+        }
     }
 
     @Override
@@ -127,7 +149,7 @@
     /* SingleAxisSwipeDetector.Listener */
 
     @Override
-    public void onDragStart(boolean start) { }
+    public void onDragStart(boolean start, float startDisplacement) { }
 
     @Override
     public boolean onDrag(float displacement) {
@@ -185,9 +207,25 @@
     protected void onCloseComplete() {
         mIsOpen = false;
         getPopupContainer().removeView(this);
+        if (mColorScrim != null) {
+            getPopupContainer().removeView(mColorScrim);
+        }
     }
 
     protected BaseDragLayer getPopupContainer() {
         return mLauncher.getDragLayer();
     }
+
+
+    protected static View createColorScrim(Context context, int bgColor) {
+        View view = new View(context);
+        view.forceHasOverlappingRendering(false);
+        view.setBackgroundColor(bgColor);
+
+        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
+        lp.ignoreInsets = true;
+        view.setLayoutParams(lp);
+
+        return view;
+    }
 }
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 0331a86..ae459e1 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -21,9 +21,8 @@
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.model.data.ItemInfo;
 
 /**
  * An interface to be used along with a context for various activities in Launcher. This allows a
@@ -57,19 +56,6 @@
 
     DeviceProfile getDeviceProfile();
 
-    /**
-     * Device profile to be used by UI elements which are shown directly on top of the wallpaper
-     * and whose presentation is tied to the wallpaper (and physical device) and not the activity
-     * configuration.
-     */
-    default DeviceProfile getWallpaperDeviceProfile() {
-        return getDeviceProfile();
-    }
-
-    default RotationMode getRotationMode() {
-        return RotationMode.NORMAL;
-    }
-
     static ActivityContext lookupContext(Context context) {
         if (context instanceof ActivityContext) {
             return (ActivityContext) context;
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
new file mode 100644
index 0000000..b4a6b14
--- /dev/null
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.views;
+
+import android.content.Context;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Handler;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+
+/**
+ * A base class for arrow tip view in launcher
+ */
+public class ArrowTipView extends AbstractFloatingView {
+
+    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
+    private static final long SHOW_DELAY_MS = 200;
+    private static final long SHOW_DURATION_MS = 300;
+    private static final long HIDE_DURATION_MS = 100;
+
+    protected final BaseDraggingActivity mActivity;
+    private final Handler mHandler = new Handler();
+    private Runnable mOnClosed;
+
+    public ArrowTipView(Context context) {
+        super(context, null, 0);
+        mActivity = BaseDraggingActivity.fromContext(context);
+        init(context);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            close(true);
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (mIsOpen) {
+            if (animate) {
+                animate().alpha(0f)
+                        .withLayer()
+                        .setStartDelay(0)
+                        .setDuration(HIDE_DURATION_MS)
+                        .setInterpolator(Interpolators.ACCEL)
+                        .withEndAction(() -> mActivity.getDragLayer().removeView(this))
+                        .start();
+            } else {
+                animate().cancel();
+                mActivity.getDragLayer().removeView(this);
+            }
+            if (mOnClosed != null) mOnClosed.run();
+            mIsOpen = false;
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    private void init(Context context) {
+        inflate(context, R.layout.arrow_toast, this);
+        setOrientation(LinearLayout.VERTICAL);
+        View dismissButton = findViewById(R.id.dismiss);
+        dismissButton.setOnClickListener(view -> {
+            handleClose(true);
+        });
+
+        View arrowView = findViewById(R.id.arrow);
+        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
+        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                arrowLp.width, arrowLp.height, false));
+        Paint arrowPaint = arrowDrawable.getPaint();
+        TypedValue typedValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
+        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+        arrowPaint.setPathEffect(new CornerPathEffect(
+                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
+        arrowView.setBackground(arrowDrawable);
+
+        mIsOpen = true;
+
+        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Show Tip with specified string and Y location
+     */
+    public ArrowTipView show(String text, int top) {
+        return show(text, Gravity.CENTER_HORIZONTAL, 0, top);
+    }
+
+    /**
+     * Show the ArrowTipView (tooltip) center, start, or end aligned.
+     *
+     * @param text The text to be shown in the tooltip.
+     * @param gravity The gravity aligns the tooltip center, start, or end.
+     * @param arrowMarginStart The margin from start to place arrow (ignored if center)
+     * @param top  The Y coordinate of the bottom of tooltip.
+     * @return The tooltip.
+     */
+    public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) {
+        ((TextView) findViewById(R.id.text)).setText(text);
+        ViewGroup parent = mActivity.getDragLayer();
+        parent.addView(this);
+
+        DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
+        params.gravity = gravity;
+        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) findViewById(
+                R.id.arrow).getLayoutParams();
+        lp.gravity = gravity;
+        if (gravity == Gravity.END) {
+            lp.setMarginEnd(parent.getMeasuredWidth() - arrowMarginStart);
+        } else if (gravity == Gravity.START) {
+            lp.setMarginStart(arrowMarginStart);
+        }
+        requestLayout();
+
+        params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left;
+        params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right;
+        post(() -> setY(top - getHeight()));
+        setAlpha(0);
+        animate()
+                .alpha(1f)
+                .withLayer()
+                .setStartDelay(SHOW_DELAY_MS)
+                .setDuration(SHOW_DURATION_MS)
+                .setInterpolator(Interpolators.DEACCEL)
+                .start();
+        return this;
+    }
+
+    /**
+     * Register a callback fired when toast is hidden
+     */
+    public ArrowTipView setOnClosedCallback(Runnable runnable) {
+        mOnClosed = runnable;
+        return this;
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index a4f6a8e..b010b4b 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -25,11 +25,9 @@
 import android.annotation.TargetApi;
 import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -54,6 +52,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TouchController;
 
 import java.io.PrintWriter;
@@ -91,13 +90,23 @@
                 }
             };
 
-    // Touch is being dispatched through the normal view dispatch system
-    private static final int TOUCH_DISPATCHING_VIEW = 1 << 0;
+    // Touch coming from normal view system is being dispatched.
+    private static final int TOUCH_DISPATCHING_FROM_VIEW = 1 << 0;
     // Touch is being dispatched through the normal view dispatch system, and started at the
-    // system gesture region
-    private static final int TOUCH_DISPATCHING_GESTURE = 1 << 1;
-    // Touch is being dispatched through a proxy from InputMonitor
-    private static final int TOUCH_DISPATCHING_PROXY = 1 << 2;
+    // system gesture region. In this case we prevent internal gesture handling and only allow
+    // normal view event handling.
+    private static final int TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION = 1 << 1;
+    // Touch coming from InputMonitor proxy is being dispatched 'only to gestures'. Note that both
+    // this and view-system can be active at the same time where view-system would go to the views,
+    // and this would go to the gestures.
+    // Note that this is not set when events are coming from proxy, but going through full dispatch
+    // process (both views and gestures) to allow view-system to easily take over in case it
+    // comes later.
+    private static final int TOUCH_DISPATCHING_FROM_PROXY = 1 << 2;
+    // ACTION_DOWN has been dispatched to child views and ACTION_UP or ACTION_CANCEL is pending.
+    // Note that the event source can either be view-dispatching or proxy-dispatching based on if
+    // TOUCH_DISPATCHING_VIEW is present or not.
+    private static final int TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS = 1 << 3;
 
     protected final float[] mTmpXY = new float[2];
     protected final float[] mTmpRectPoints = new float[4];
@@ -110,12 +119,8 @@
     protected final T mActivity;
     private final MultiValueAlpha mMultiValueAlpha;
     private final WallpaperManager mWallpaperManager;
-    private final BroadcastReceiver mWallpaperChangeReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            onWallpaperChanged();
-        }
-    };
+    private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
+            new SimpleBroadcastReceiver(this::onWallpaperChanged);
     private final String[] mWallpapersWithoutSysuiScrims;
 
     // All the touch controllers for the view
@@ -139,6 +144,11 @@
     }
 
     /**
+     * Called to reinitialize touch controllers.
+     */
+    public abstract void recreateControllers();
+
+    /**
      * Same as {@link #isEventOverView(View, MotionEvent, View)} where evView == this drag layer.
      */
     public boolean isEventOverView(View view, MotionEvent ev) {
@@ -172,9 +182,25 @@
         return findActiveController(ev);
     }
 
+    private boolean isEventInLauncher(MotionEvent ev) {
+        final float x = ev.getX();
+        final float y = ev.getY();
+
+        return x >= mSystemGestureRegion.left && x < getWidth() - mSystemGestureRegion.right
+                && y >= mSystemGestureRegion.top && y < getHeight() - mSystemGestureRegion.bottom;
+    }
+
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "findControllerToHandleTouch ev=" + ev
+                    + ", isEventInLauncher=" + isEventInLauncher(ev)
+                    + ", topOpenView=" + AbstractFloatingView.getTopOpenView(mActivity));
+        }
+
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
-        if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
+        if (topView != null
+                && (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
+                && topView.onControllerInterceptTouchEvent(ev)) {
             return topView;
         }
 
@@ -188,7 +214,8 @@
 
     protected boolean findActiveController(MotionEvent ev) {
         mActiveController = null;
-        if ((mTouchDispatchState & (TOUCH_DISPATCHING_GESTURE | TOUCH_DISPATCHING_PROXY)) == 0) {
+        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
             mActiveController = findControllerToHandleTouch(ev);
@@ -267,28 +294,28 @@
     public boolean dispatchTouchEvent(MotionEvent ev) {
         switch (ev.getAction()) {
             case ACTION_DOWN: {
-                float x = ev.getX();
-                float y = ev.getY();
-                mTouchDispatchState |= TOUCH_DISPATCHING_VIEW;
+                if ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0) {
+                    // Cancel the previous touch
+                    int action = ev.getAction();
+                    ev.setAction(ACTION_CANCEL);
+                    super.dispatchTouchEvent(ev);
+                    ev.setAction(action);
+                }
+                mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW
+                        | TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
 
-                if ((y < mSystemGestureRegion.top
-                        || x < mSystemGestureRegion.left
-                        || x > (getWidth() - mSystemGestureRegion.right)
-                        || y > (getHeight() - mSystemGestureRegion.bottom))) {
-                    mTouchDispatchState |= TOUCH_DISPATCHING_GESTURE;
+                if (isEventInLauncher(ev)) {
+                    mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
                 } else {
-                    mTouchDispatchState &= ~TOUCH_DISPATCHING_GESTURE;
+                    mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
                 }
                 break;
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE,
-                            "BaseDragLayer.ACTION_UP/CANCEL " + ev);
-                }
-                mTouchDispatchState &= ~TOUCH_DISPATCHING_GESTURE;
-                mTouchDispatchState &= ~TOUCH_DISPATCHING_VIEW;
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW;
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
                 break;
         }
         super.dispatchTouchEvent(ev);
@@ -298,43 +325,53 @@
     }
 
     /**
-     * Called before we are about to receive proxy events.
-     *
-     * @return false if we can't handle proxy at this time
-     */
-    public boolean prepareProxyEventStarting() {
-        mProxyTouchController = null;
-        if ((mTouchDispatchState & TOUCH_DISPATCHING_VIEW) != 0 && mActiveController != null) {
-            // We are already dispatching using view system and have an active controller, we can't
-            // handle another controller.
-
-            // This flag was already cleared in proxy ACTION_UP or ACTION_CANCEL. Added here just
-            // to be safe
-            mTouchDispatchState &= ~TOUCH_DISPATCHING_PROXY;
-            return false;
-        }
-
-        mTouchDispatchState |= TOUCH_DISPATCHING_PROXY;
-        return true;
-    }
-
-    /**
      * Proxies the touch events to the gesture handlers
      */
-    public boolean proxyTouchEvent(MotionEvent ev) {
-        boolean handled;
-        if (mProxyTouchController != null) {
-            handled = mProxyTouchController.onControllerTouchEvent(ev);
+    public boolean proxyTouchEvent(MotionEvent ev, boolean allowViewDispatch) {
+        int actionMasked = ev.getActionMasked();
+        boolean isViewDispatching = (mTouchDispatchState & TOUCH_DISPATCHING_FROM_VIEW) != 0;
+
+        // Only do view dispatch if another view-dispatching is not running, or we already started
+        // proxy-dispatching before. Note that view-dispatching can always take over the proxy
+        // dispatching at anytime, but not vice-versa.
+        allowViewDispatch = allowViewDispatch && !isViewDispatching
+                && (actionMasked == ACTION_DOWN
+                    || ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0));
+
+        if (allowViewDispatch) {
+            mTouchDispatchState |= TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
+            super.dispatchTouchEvent(ev);
+
+            if (actionMasked == ACTION_UP || actionMasked == ACTION_CANCEL) {
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
+            }
+            return true;
         } else {
-            mProxyTouchController = findControllerToHandleTouch(ev);
-            handled = mProxyTouchController != null;
+            boolean handled;
+            if (mProxyTouchController != null) {
+                handled = mProxyTouchController.onControllerTouchEvent(ev);
+            } else {
+                if (actionMasked == ACTION_DOWN) {
+                    if (isViewDispatching && mActiveController != null) {
+                        // A controller is already active, we can't initiate our own controller
+                        mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
+                    } else {
+                        // We will control the handler via proxy
+                        mTouchDispatchState |= TOUCH_DISPATCHING_FROM_PROXY;
+                    }
+                }
+                if ((mTouchDispatchState & TOUCH_DISPATCHING_FROM_PROXY) != 0) {
+                    mProxyTouchController = findControllerToHandleTouch(ev);
+                }
+                handled = mProxyTouchController != null;
+            }
+            if (actionMasked == ACTION_UP || actionMasked == ACTION_CANCEL) {
+                mProxyTouchController = null;
+                mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
+            }
+            return handled;
         }
-        int action = ev.getAction();
-        if (action == ACTION_UP || action == ACTION_CANCEL) {
-            mProxyTouchController = null;
-            mTouchDispatchState &= ~TOUCH_DISPATCHING_PROXY;
-        }
-        return handled;
     }
 
     /**
@@ -539,9 +576,8 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mActivity.registerReceiver(mWallpaperChangeReceiver,
-                new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED));
-        onWallpaperChanged();
+        mWallpaperChangeReceiver.register(mActivity, Intent.ACTION_WALLPAPER_CHANGED);
+        onWallpaperChanged(null);
     }
 
     @Override
@@ -550,7 +586,7 @@
         mActivity.unregisterReceiver(mWallpaperChangeReceiver);
     }
 
-    private void onWallpaperChanged() {
+    private void onWallpaperChanged(Intent unusedBroadcastIntent) {
         WallpaperInfo newWallpaperInfo = mWallpaperManager.getWallpaperInfo();
         boolean oldAllowSysuiScrims = mAllowSysuiScrims;
         mAllowSysuiScrims = computeAllowSysuiScrims(newWallpaperInfo);
diff --git a/src/com/android/launcher3/views/BottomUserEducationView.java b/src/com/android/launcher3/views/BottomUserEducationView.java
deleted file mode 100644
index bdc69af..0000000
--- a/src/com/android/launcher3/views/BottomUserEducationView.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.views;
-
-import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.TouchDelegate;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-
-public class BottomUserEducationView extends AbstractSlideInView implements Insettable {
-
-    private static final String KEY_SHOWED_BOTTOM_USER_EDUCATION = "showed_bottom_user_education";
-
-    private static final int DEFAULT_CLOSE_DURATION = 200;
-
-    private final Rect mInsets = new Rect();
-
-    private View mCloseButton;
-
-    public BottomUserEducationView(Context context, AttributeSet attr) {
-        this(context, attr, 0);
-    }
-
-    public BottomUserEducationView(Context context, AttributeSet attrs,
-            int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mContent = this;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mCloseButton = findViewById(R.id.close_bottom_user_tip);
-        mCloseButton.setOnClickListener(view -> handleClose(true));
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        setTranslationShift(mTranslationShift);
-        expandTouchAreaOfCloseButton();
-    }
-
-    @Override
-    public void logActionCommand(int command) {
-        // Since this is on-boarding popup, it is not a user controlled action.
-    }
-
-    @Override
-    public int getLogContainerType() {
-        return ContainerType.TIP;
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_ON_BOARD_POPUP) != 0;
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        // Extend behind left, right, and bottom insets.
-        int leftInset = insets.left - mInsets.left;
-        int rightInset = insets.right - mInsets.right;
-        int bottomInset = insets.bottom - mInsets.bottom;
-        mInsets.set(insets);
-        setPadding(getPaddingLeft() + leftInset, getPaddingTop(),
-                getPaddingRight() + rightInset, getPaddingBottom() + bottomInset);
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        handleClose(animate, DEFAULT_CLOSE_DURATION);
-        if (animate) {
-            // We animate only when the user is visible, which is a proxy for an explicit
-            // close action.
-            mLauncher.getSharedPrefs().edit()
-                    .putBoolean(KEY_SHOWED_BOTTOM_USER_EDUCATION, true).apply();
-            sendCustomAccessibilityEvent(
-                    BottomUserEducationView.this,
-                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                    getContext().getString(R.string.bottom_work_tab_user_education_closed));
-        }
-    }
-
-    private void open(boolean animate) {
-        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
-            return;
-        }
-        mIsOpen = true;
-        if (animate) {
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mOpenCloseAnimator.start();
-        } else {
-            setTranslationShift(TRANSLATION_SHIFT_OPENED);
-        }
-    }
-
-    public static void showIfNeeded(Launcher launcher) {
-        if (launcher.getSharedPrefs().getBoolean(KEY_SHOWED_BOTTOM_USER_EDUCATION, false)) {
-            return;
-        }
-
-        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
-        BottomUserEducationView bottomUserEducationView =
-                (BottomUserEducationView) layoutInflater.inflate(
-                        R.layout.work_tab_bottom_user_education_view, launcher.getDragLayer(),
-                        false);
-        launcher.getDragLayer().addView(bottomUserEducationView);
-        bottomUserEducationView.open(true);
-    }
-
-    private void expandTouchAreaOfCloseButton() {
-        Rect hitRect = new Rect();
-        mCloseButton.getHitRect(hitRect);
-        hitRect.left -= mCloseButton.getWidth();
-        hitRect.top -= mCloseButton.getHeight();
-        hitRect.right += mCloseButton.getWidth();
-        hitRect.bottom += mCloseButton.getHeight();
-        View parent = (View) mCloseButton.getParent();
-        parent.setTouchDelegate(new TouchDelegate(hitRect, mCloseButton));
-    }
-}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
new file mode 100644
index 0000000..1a8e11b
--- /dev/null
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -0,0 +1,350 @@
+/*
+ * 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.views;
+
+import static com.android.launcher3.Utilities.mapToRange;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InsettableFrameLayout.LayoutParams;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
+import com.android.launcher3.graphics.IconShape;
+
+/**
+ * A view used to draw both layers of an {@link AdaptiveIconDrawable}.
+ * Supports springing just the foreground layer.
+ * Supports clipping the icon to/from its icon shape.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class ClipIconView extends View implements ClipPathView {
+
+    private static final Rect sTmpRect = new Rect();
+
+    // We spring the foreground drawable relative to the icon's movement in the DragLayer.
+    // We then use these two factor values to scale the movement of the fg within this view.
+    private static final int FG_TRANS_X_FACTOR = 60;
+    private static final int FG_TRANS_Y_FACTOR = 75;
+
+    private static final FloatPropertyCompat<ClipIconView> mFgTransYProperty =
+            new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransY") {
+                @Override
+                public float getValue(ClipIconView view) {
+                    return view.mFgTransY;
+                }
+
+                @Override
+                public void setValue(ClipIconView view, float transY) {
+                    view.mFgTransY = transY;
+                    view.invalidate();
+                }
+            };
+
+    private static final FloatPropertyCompat<ClipIconView> mFgTransXProperty =
+            new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransX") {
+                @Override
+                public float getValue(ClipIconView view) {
+                    return view.mFgTransX;
+                }
+
+                @Override
+                public void setValue(ClipIconView view, float transX) {
+                    view.mFgTransX = transX;
+                    view.invalidate();
+                }
+            };
+
+    private final Launcher mLauncher;
+    private final int mBlurSizeOutline;
+    private final boolean mIsRtl;
+
+    private @Nullable Drawable mForeground;
+    private @Nullable Drawable mBackground;
+
+    private boolean mIsAdaptiveIcon = false;
+
+    private ValueAnimator mRevealAnimator;
+
+    private final Rect mStartRevealRect = new Rect();
+    private final Rect mEndRevealRect = new Rect();
+    private Path mClipPath;
+    private float mTaskCornerRadius;
+
+    private final Rect mOutline = new Rect();
+    private final Rect mFinalDrawableBounds = new Rect();
+
+    private final SpringAnimation mFgSpringY;
+    private float mFgTransY;
+    private final SpringAnimation mFgSpringX;
+    private float mFgTransX;
+
+    public ClipIconView(Context context) {
+        this(context, null);
+    }
+
+    public ClipIconView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        mBlurSizeOutline = getResources().getDimensionPixelSize(
+                R.dimen.blur_size_medium_outline);
+        mIsRtl = Utilities.isRtl(getResources());
+
+        mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
+                .setSpring(new SpringForce()
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                        .setStiffness(SpringForce.STIFFNESS_LOW));
+        mFgSpringY = new SpringAnimation(this, mFgTransYProperty)
+                .setSpring(new SpringForce()
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                        .setStiffness(SpringForce.STIFFNESS_LOW));
+    }
+
+    void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
+            boolean isOpening, float scale, float minSize, LayoutParams parentLp,
+            boolean isVerticalBarLayout) {
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        float dX = mIsRtl
+                ? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
+                : rect.left - parentLp.getMarginStart();
+        float dY = rect.top - parentLp.topMargin;
+
+        // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
+        float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
+        float shapeRevealProgress = Utilities.boundToRange(mapToRange(
+                Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
+                LINEAR), 0, 1);
+
+        if (isVerticalBarLayout) {
+            mOutline.right = (int) (rect.width() / scale);
+        } else {
+            mOutline.bottom = (int) (rect.height() / scale);
+        }
+
+        mTaskCornerRadius = cornerRadius / scale;
+        if (mIsAdaptiveIcon) {
+            if (!isOpening && progress >= shapeProgressStart) {
+                if (mRevealAnimator == null) {
+                    mRevealAnimator = (ValueAnimator) IconShape.getShape().createRevealAnimator(
+                            this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening);
+                    mRevealAnimator.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mRevealAnimator = null;
+                        }
+                    });
+                    mRevealAnimator.start();
+                    // We pause here so we can set the current fraction ourselves.
+                    mRevealAnimator.pause();
+                }
+                mRevealAnimator.setCurrentFraction(shapeRevealProgress);
+            }
+
+            float drawableScale = (isVerticalBarLayout ? mOutline.width() : mOutline.height())
+                    / minSize;
+            setBackgroundDrawableBounds(drawableScale, isVerticalBarLayout);
+            if (isOpening) {
+                // Center align foreground
+                int height = mFinalDrawableBounds.height();
+                int width = mFinalDrawableBounds.width();
+                int diffY = isVerticalBarLayout ? 0
+                        : (int) (((height * drawableScale) - height) / 2);
+                int diffX = isVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
+                        : 0;
+                sTmpRect.set(mFinalDrawableBounds);
+                sTmpRect.offset(diffX, diffY);
+                mForeground.setBounds(sTmpRect);
+            } else {
+                // Spring the foreground relative to the icon's movement within the DragLayer.
+                int diffX = (int) (dX / dp.availableWidthPx * FG_TRANS_X_FACTOR);
+                int diffY = (int) (dY / dp.availableHeightPx * FG_TRANS_Y_FACTOR);
+
+                mFgSpringX.animateToFinalPosition(diffX);
+                mFgSpringY.animateToFinalPosition(diffY);
+            }
+        }
+        invalidate();
+        invalidateOutline();
+    }
+
+    private void setBackgroundDrawableBounds(float scale, boolean isVerticalBarLayout) {
+        sTmpRect.set(mFinalDrawableBounds);
+        Utilities.scaleRectAboutCenter(sTmpRect, scale);
+        // Since the drawable is at the top of the view, we need to offset to keep it centered.
+        if (isVerticalBarLayout) {
+            sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
+        } else {
+            sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
+        }
+        mBackground.setBounds(sTmpRect);
+    }
+
+    protected void endReveal() {
+        if (mRevealAnimator != null) {
+            mRevealAnimator.end();
+        }
+    }
+
+    void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
+            boolean isVerticalBarLayout) {
+        mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
+        if (mIsAdaptiveIcon) {
+            boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
+
+            AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
+            Drawable background = adaptiveIcon.getBackground();
+            if (background == null) {
+                background = new ColorDrawable(Color.TRANSPARENT);
+            }
+            mBackground = background;
+            Drawable foreground = adaptiveIcon.getForeground();
+            if (foreground == null) {
+                foreground = new ColorDrawable(Color.TRANSPARENT);
+            }
+            mForeground = foreground;
+
+            final int originalHeight = lp.height;
+            final int originalWidth = lp.width;
+
+            int blurMargin = mBlurSizeOutline / 2;
+            mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
+
+            if (!isFolderIcon) {
+                mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
+            }
+            mForeground.setBounds(mFinalDrawableBounds);
+            mBackground.setBounds(mFinalDrawableBounds);
+
+            mStartRevealRect.set(0, 0, originalWidth, originalHeight);
+
+            if (!isFolderIcon) {
+                Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale());
+            }
+
+            float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
+            if (isVerticalBarLayout) {
+                lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
+            } else {
+                lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
+            }
+
+            int left = mIsRtl
+                    ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+                    : lp.leftMargin;
+            layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
+
+            float scale = Math.max((float) lp.height / originalHeight,
+                    (float) lp.width / originalWidth);
+            float bgDrawableStartScale;
+            if (isOpening) {
+                bgDrawableStartScale = 1f;
+                mOutline.set(0, 0, originalWidth, originalHeight);
+            } else {
+                bgDrawableStartScale = scale;
+                mOutline.set(0, 0, lp.width, lp.height);
+            }
+            setBackgroundDrawableBounds(bgDrawableStartScale, isVerticalBarLayout);
+            mEndRevealRect.set(0, 0, lp.width, lp.height);
+            setOutlineProvider(new ViewOutlineProvider() {
+                @Override
+                public void getOutline(View view, Outline outline) {
+                    outline.setRoundRect(mOutline, mTaskCornerRadius);
+                }
+            });
+            setClipToOutline(true);
+        } else {
+            setBackground(drawable);
+            setClipToOutline(false);
+        }
+
+        invalidate();
+        invalidateOutline();
+    }
+
+    @Override
+    public void setClipPath(Path clipPath) {
+        mClipPath = clipPath;
+        invalidate();
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        int count = canvas.save();
+        if (mClipPath != null) {
+            canvas.clipPath(mClipPath);
+        }
+        super.draw(canvas);
+        if (mBackground != null) {
+            mBackground.draw(canvas);
+        }
+        if (mForeground != null) {
+            int count2 = canvas.save();
+            canvas.translate(mFgTransX, mFgTransY);
+            mForeground.draw(canvas);
+            canvas.restoreToCount(count2);
+        }
+        canvas.restoreToCount(count);
+    }
+
+    void recycle() {
+        setBackground(null);
+        mIsAdaptiveIcon = false;
+        mForeground = null;
+        mBackground = null;
+        mClipPath = null;
+        mFinalDrawableBounds.setEmpty();
+        if (mRevealAnimator != null) {
+            mRevealAnimator.cancel();
+        }
+        mRevealAnimator = null;
+        mTaskCornerRadius = 0;
+        mOutline.setEmpty();
+        mFgTransY = 0;
+        mFgSpringX.cancel();
+        mFgTransX = 0;
+        mFgSpringY.cancel();
+    }
+}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 49d94f0..177aff4 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -18,27 +18,19 @@
 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
-import static com.android.launcher3.Utilities.mapToRange;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 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.content.Context;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Outline;
-import android.graphics.Path;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.CancellationSignal;
@@ -46,38 +38,35 @@
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.InsettableFrameLayout.LayoutParams;
-import com.android.launcher3.ItemInfo;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.ShiftedBitmapDrawable;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.testing.TestProtocol;
 
 /**
  * A view that is created to look like another view with the purpose of creating fluid animations.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class FloatingIconView extends View implements
-        Animator.AnimatorListener, ClipPathView, OnGlobalLayoutListener {
+public class FloatingIconView extends FrameLayout implements
+        Animator.AnimatorListener, OnGlobalLayoutListener {
 
     private static final String TAG = FloatingIconView.class.getSimpleName();
 
@@ -86,80 +75,32 @@
 
     public static final float SHAPE_PROGRESS_DURATION = 0.10f;
     private static final int FADE_DURATION_MS = 200;
-    private static final Rect sTmpRect = new Rect();
     private static final RectF sTmpRectF = new RectF();
     private static final Object[] sTmpObjArray = new Object[1];
 
-    // We spring the foreground drawable relative to the icon's movement in the DragLayer.
-    // We then use these two factor values to scale the movement of the fg within this view.
-    private static final int FG_TRANS_X_FACTOR = 60;
-    private static final int FG_TRANS_Y_FACTOR = 75;
-
-    private static final FloatPropertyCompat<FloatingIconView> mFgTransYProperty
-            = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransY") {
-        @Override
-        public float getValue(FloatingIconView view) {
-            return view.mFgTransY;
-        }
-
-        @Override
-        public void setValue(FloatingIconView view, float transY) {
-            view.mFgTransY = transY;
-            view.invalidate();
-        }
-    };
-
-    private static final FloatPropertyCompat<FloatingIconView> mFgTransXProperty
-            = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransX") {
-        @Override
-        public float getValue(FloatingIconView view) {
-            return view.mFgTransX;
-        }
-
-        @Override
-        public void setValue(FloatingIconView view, float transX) {
-            view.mFgTransX = transX;
-            view.invalidate();
-        }
-    };
-
     private Runnable mEndRunnable;
     private CancellationSignal mLoadIconSignal;
 
     private final Launcher mLauncher;
-    private final int mBlurSizeOutline;
     private final boolean mIsRtl;
 
     private boolean mIsVerticalBarLayout = false;
-    private boolean mIsAdaptiveIcon = false;
     private boolean mIsOpening;
 
     private IconLoadResult mIconLoadResult;
 
+    private ClipIconView mClipIconView;
     private @Nullable Drawable mBadge;
-    private @Nullable Drawable mForeground;
-    private @Nullable Drawable mBackground;
-    private float mRotation;
-    private ValueAnimator mRevealAnimator;
-    private final Rect mStartRevealRect = new Rect();
-    private final Rect mEndRevealRect = new Rect();
-    private Path mClipPath;
-    private float mTaskCornerRadius;
 
     private View mOriginalIcon;
     private RectF mPositionOut;
     private Runnable mOnTargetChangeRunnable;
 
-    private final Rect mOutline = new Rect();
     private final Rect mFinalDrawableBounds = new Rect();
 
     private AnimatorSet mFadeAnimatorSet;
     private ListenerView mListenerView;
-
-    private final SpringAnimation mFgSpringY;
-    private float mFgTransY;
-    private final SpringAnimation mFgSpringX;
-    private float mFgTransX;
+    private Runnable mFastFinishRunnable;
 
     public FloatingIconView(Context context) {
         this(context, null);
@@ -172,19 +113,11 @@
     public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mLauncher = Launcher.getLauncher(context);
-        mBlurSizeOutline = getResources().getDimensionPixelSize(
-                R.dimen.blur_size_medium_outline);
         mIsRtl = Utilities.isRtl(getResources());
         mListenerView = new ListenerView(context, attrs);
-
-        mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
-                .setSpring(new SpringForce()
-                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                .setStiffness(SpringForce.STIFFNESS_LOW));
-        mFgSpringY = new SpringAnimation(this, mFgTransYProperty)
-                .setSpring(new SpringForce()
-                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                        .setStiffness(SpringForce.STIFFNESS_LOW));
+        mClipIconView = new ClipIconView(context, attrs);
+        addView(mClipIconView);
+        setWillNotDraw(false);
     }
 
     @Override
@@ -192,7 +125,6 @@
         super.onAttachedToWindow();
         if (!mIsOpening) {
             getViewTreeObserver().addOnGlobalLayoutListener(this);
-            mLauncher.getRotationHelper().setCurrentTransitionRequest(REQUEST_LOCK);
         }
     }
 
@@ -213,10 +145,12 @@
             float cornerRadius, boolean isOpening) {
         setAlpha(alpha);
 
-        LayoutParams lp = (LayoutParams) getLayoutParams();
+        InsettableFrameLayout.LayoutParams lp =
+                (InsettableFrameLayout.LayoutParams) getLayoutParams();
+
+        DeviceProfile dp = mLauncher.getDeviceProfile();
         float dX = mIsRtl
-                ? rect.left
-                - (mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width)
+                ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
                 : rect.left - lp.getMarginStart();
         float dY = rect.top - lp.topMargin;
         setTranslationX(dX);
@@ -227,69 +161,15 @@
         float scaleY = rect.height() / minSize;
         float scale = Math.max(1f, Math.min(scaleX, scaleY));
 
+        mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
+                minSize, lp, mIsVerticalBarLayout);
+
         setPivotX(0);
         setPivotY(0);
         setScaleX(scale);
         setScaleY(scale);
 
-        // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
-        float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
-        float shapeRevealProgress = Utilities.boundToRange(mapToRange(
-                Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
-                LINEAR), 0, 1);
-
-        if (mIsVerticalBarLayout) {
-            mOutline.right = (int) (rect.width() / scale);
-        } else {
-            mOutline.bottom = (int) (rect.height() / scale);
-        }
-
-        mTaskCornerRadius = cornerRadius / scale;
-        if (mIsAdaptiveIcon) {
-            if (!isOpening && progress >= shapeProgressStart) {
-                if (mRevealAnimator == null) {
-                    mRevealAnimator = (ValueAnimator) IconShape.getShape().createRevealAnimator(
-                            this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening);
-                    mRevealAnimator.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            mRevealAnimator = null;
-                        }
-                    });
-                    mRevealAnimator.start();
-                    // We pause here so we can set the current fraction ourselves.
-                    mRevealAnimator.pause();
-                }
-                mRevealAnimator.setCurrentFraction(shapeRevealProgress);
-            }
-
-            float drawableScale = (mIsVerticalBarLayout ? mOutline.width() : mOutline.height())
-                    / minSize;
-            setBackgroundDrawableBounds(drawableScale);
-            if (isOpening) {
-                // Center align foreground
-                int height = mFinalDrawableBounds.height();
-                int width = mFinalDrawableBounds.width();
-                int diffY = mIsVerticalBarLayout ? 0
-                        : (int) (((height * drawableScale) - height) / 2);
-                int diffX = mIsVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
-                        : 0;
-                sTmpRect.set(mFinalDrawableBounds);
-                sTmpRect.offset(diffX, diffY);
-                mForeground.setBounds(sTmpRect);
-            } else {
-                // Spring the foreground relative to the icon's movement within the DragLayer.
-                int diffX = (int) (dX / mLauncher.getDeviceProfile().availableWidthPx
-                        * FG_TRANS_X_FACTOR);
-                int diffY = (int) (dY / mLauncher.getDeviceProfile().availableHeightPx
-                        * FG_TRANS_Y_FACTOR);
-
-                mFgSpringX.animateToFinalPosition(diffX);
-                mFgSpringY.animateToFinalPosition(diffY);
-            }
-        }
         invalidate();
-        invalidateOutline();
     }
 
     @Override
@@ -301,9 +181,7 @@
             mEndRunnable.run();
         } else {
             // End runnable also ends the reveal animator, so we manually handle it here.
-            if (mRevealAnimator != null) {
-                mRevealAnimator.end();
-            }
+            mClipIconView.endReveal();
         }
     }
 
@@ -314,24 +192,25 @@
      * @param positionOut Rect that will hold the size and position of v.
      */
     private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) {
-        float rotation = getLocationBoundsForView(launcher, v, isOpening, positionOut);
-        final LayoutParams lp = new LayoutParams(
+        getLocationBoundsForView(launcher, v, isOpening, positionOut);
+        final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams(
                 Math.round(positionOut.width()),
                 Math.round(positionOut.height()));
-        updatePosition(rotation, positionOut, lp);
+        updatePosition(positionOut, lp);
         setLayoutParams(lp);
+
+        mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height));
     }
 
-    private void updatePosition(float rotation, RectF position, LayoutParams lp) {
-        mRotation = rotation;
-        mPositionOut.set(position);
+    private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
+        mPositionOut.set(pos);
         lp.ignoreInsets = true;
         // Position the floating view exactly on top of the original
-        lp.topMargin = Math.round(position.top);
+        lp.topMargin = Math.round(pos.top);
         if (mIsRtl) {
-            lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - position.right));
+            lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right));
         } else {
-            lp.setMarginStart(Math.round(position.left));
+            lp.setMarginStart(Math.round(pos.left));
         }
         // Set the properties here already to make sure they are available when running the first
         // animation frame.
@@ -346,7 +225,7 @@
      * - For DeepShortcutView, we return the bounds of the icon view.
      * - For BubbleTextView, we return the icon bounds.
      */
-    private static float getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
             RectF outRect) {
         boolean ignoreTransform = !isOpening;
         if (v instanceof DeepShortcutView) {
@@ -357,7 +236,7 @@
             ignoreTransform = false;
         }
         if (v == null) {
-            return 0;
+            return;
         }
 
         Rect iconBounds = new Rect();
@@ -371,15 +250,13 @@
 
         float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
                 iconBounds.bottom};
-        float[] rotation = new float[] {0};
         Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
-                false, ignoreTransform, rotation);
+                false, ignoreTransform);
         outRect.set(
                 Math.min(points[0], points[2]),
                 Math.min(points[1], points[3]),
                 Math.max(points[0], points[2]),
                 Math.max(points[1], points[3]));
-        return rotation[0];
     }
 
     /**
@@ -399,7 +276,8 @@
         Drawable drawable = null;
         Drawable badge = null;
         boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get()
-                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                && !info.isDisabled(); // Use original icon for disabled icons.
         Drawable btvIcon = originalView instanceof BubbleTextView
                 ? ((BubbleTextView) originalView).getIcon() : null;
         if (info instanceof SystemShortcut) {
@@ -411,11 +289,10 @@
                 drawable = originalView.getBackground();
             }
         } else {
-            boolean isFolderIcon = originalView instanceof FolderIcon;
-            int width = isFolderIcon ? originalView.getWidth() : (int) pos.width();
-            int height = isFolderIcon ? originalView.getHeight() : (int) pos.height();
+            int width = (int) pos.width();
+            int height = (int) pos.height();
             if (supportsAdaptiveIcons) {
-                drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
+                drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
                 if (drawable instanceof AdaptiveIconDrawable) {
                     badge = getBadge(l, info, sTmpObjArray[0]);
                 } else {
@@ -428,7 +305,7 @@
                     // Similar to DragView, we simply use the BubbleTextView icon here.
                     drawable = btvIcon;
                 } else {
-                    drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
+                    drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
                 }
             }
         }
@@ -450,110 +327,42 @@
     /**
      * Sets the drawables of the {@param originalView} onto this view.
      *
-     * @param originalView The View that the FloatingIconView will replace.
      * @param drawable The drawable of the original view.
      * @param badge The badge of the original view.
      * @param iconOffset The amount of offset needed to match this view with the original view.
      */
     @UiThread
-    private void setIcon(View originalView, @Nullable Drawable drawable, @Nullable Drawable badge,
-            int iconOffset) {
+    private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge, int iconOffset) {
+        final InsettableFrameLayout.LayoutParams lp =
+                (InsettableFrameLayout.LayoutParams) getLayoutParams();
         mBadge = badge;
-
-        mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
-        if (mIsAdaptiveIcon) {
-            boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
-
-            AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
-            Drawable background = adaptiveIcon.getBackground();
-            if (background == null) {
-                background = new ColorDrawable(Color.TRANSPARENT);
-            }
-            mBackground = background;
-            Drawable foreground = adaptiveIcon.getForeground();
-            if (foreground == null) {
-                foreground = new ColorDrawable(Color.TRANSPARENT);
-            }
-            mForeground = foreground;
-
-            final LayoutParams lp = (LayoutParams) getLayoutParams();
+        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout);
+        if (drawable instanceof AdaptiveIconDrawable) {
             final int originalHeight = lp.height;
             final int originalWidth = lp.width;
 
-            int blurMargin = mBlurSizeOutline / 2;
             mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
 
-            if (!isFolderIcon) {
-                mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
-            }
-            mForeground.setBounds(mFinalDrawableBounds);
-            mBackground.setBounds(mFinalDrawableBounds);
-
-            mStartRevealRect.set(0, 0, originalWidth, originalHeight);
-
-            if (mBadge != null) {
-                mBadge.setBounds(mStartRevealRect);
-                if (!mIsOpening && !isFolderIcon) {
-                    DRAWABLE_ALPHA.set(mBadge, 0);
-                }
-            }
-
-            if (isFolderIcon) {
-                ((FolderIcon) originalView).getPreviewBounds(sTmpRect);
-                float bgStroke = ((FolderIcon) originalView).getBackgroundStrokeWidth();
-                if (mForeground instanceof ShiftedBitmapDrawable) {
-                    ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mForeground;
-                    sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
-                    sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
-                }
-                if (mBadge instanceof ShiftedBitmapDrawable) {
-                    ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mBadge;
-                    sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
-                    sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
-                }
-            } else {
-                Utilities.scaleRectAboutCenter(mStartRevealRect,
-                        IconShape.getNormalizationScale());
-            }
-
             float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
             if (mIsVerticalBarLayout) {
                 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
             } else {
                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
             }
+            setLayoutParams(lp);
 
-            int left = mIsRtl
-                    ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
-                    : lp.leftMargin;
-            layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
+            final LayoutParams clipViewLp = (LayoutParams) mClipIconView.getLayoutParams();
+            final int clipViewOgHeight = clipViewLp.height;
+            final int clipViewOgWidth = clipViewLp.width;
+            clipViewLp.width = lp.width;
+            clipViewLp.height = lp.height;
+            mClipIconView.setLayoutParams(clipViewLp);
 
-            float scale = Math.max((float) lp.height / originalHeight,
-                    (float) lp.width / originalWidth);
-            float bgDrawableStartScale;
-            if (mIsOpening) {
-                bgDrawableStartScale = 1f;
-                mOutline.set(0, 0, originalWidth, originalHeight);
-            } else {
-                bgDrawableStartScale = scale;
-                mOutline.set(0, 0, lp.width, lp.height);
+            if (mBadge != null) {
+                mBadge.setBounds(0, 0, clipViewOgWidth, clipViewOgHeight);
             }
-            setBackgroundDrawableBounds(bgDrawableStartScale);
-            mEndRevealRect.set(0, 0, lp.width, lp.height);
-            setOutlineProvider(new ViewOutlineProvider() {
-                @Override
-                public void getOutline(View view, Outline outline) {
-                    outline.setRoundRect(mOutline, mTaskCornerRadius);
-                }
-            });
-            setClipToOutline(true);
-        } else {
-            setBackground(drawable);
-            setClipToOutline(false);
         }
-
         invalidate();
-        invalidateOutline();
     }
 
     /**
@@ -570,7 +379,7 @@
 
         synchronized (mIconLoadResult) {
             if (mIconLoadResult.isIconLoaded) {
-                setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
+                setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
                         mIconLoadResult.iconOffset);
                 hideOriginalView(originalView);
             } else {
@@ -579,8 +388,9 @@
                         return;
                     }
 
-                    setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
+                    setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
                             mIconLoadResult.iconOffset);
+
                     setVisibility(VISIBLE);
                     hideOriginalView(originalView);
                 };
@@ -598,23 +408,12 @@
         }
     }
 
-    private void setBackgroundDrawableBounds(float scale) {
-        sTmpRect.set(mFinalDrawableBounds);
-        Utilities.scaleRectAboutCenter(sTmpRect, scale);
-        // Since the drawable is at the top of the view, we need to offset to keep it centered.
-        if (mIsVerticalBarLayout) {
-            sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
-        } else {
-            sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
-        }
-        mBackground.setBounds(sTmpRect);
-    }
-
     @WorkerThread
     @SuppressWarnings("WrongThread")
     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
-                !(drawable instanceof AdaptiveIconDrawable)) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+                || !(drawable instanceof AdaptiveIconDrawable)
+                || (drawable instanceof FolderAdaptiveIcon)) {
             return 0;
         }
         int blurSizeOutline =
@@ -638,38 +437,28 @@
     }
 
     @Override
-    public void setClipPath(Path clipPath) {
-        mClipPath = clipPath;
-        invalidate();
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        int count = canvas.save();
-        canvas.rotate(mRotation,
-                mFinalDrawableBounds.exactCenterX(), mFinalDrawableBounds.exactCenterY());
-        if (mClipPath != null) {
-            canvas.clipPath(mClipPath);
-        }
-        super.draw(canvas);
-        if (mBackground != null) {
-            mBackground.draw(canvas);
-        }
-        if (mForeground != null) {
-            int count2 = canvas.save();
-            canvas.translate(mFgTransX, mFgTransY);
-            mForeground.draw(canvas);
-            canvas.restoreToCount(count2);
-        }
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
         if (mBadge != null) {
             mBadge.draw(canvas);
         }
-        canvas.restoreToCount(count);
+    }
+
+    /**
+     * Sets a runnable that is called after a call to {@link #fastFinish()}.
+     */
+    public void setFastFinishRunnable(Runnable runnable) {
+        mFastFinishRunnable = runnable;
     }
 
     public void fastFinish() {
+        if (mFastFinishRunnable != null) {
+            mFastFinishRunnable.run();
+            mFastFinishRunnable = null;
+        }
         if (mLoadIconSignal != null) {
             mLoadIconSignal.cancel();
+            mLoadIconSignal = null;
         }
         if (mEndRunnable != null) {
             mEndRunnable.run();
@@ -701,10 +490,10 @@
     @Override
     public void onGlobalLayout() {
         if (mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
-            float rotation = getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening,
+            getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening,
                     sTmpRectF);
-            if (rotation != mRotation || !sTmpRectF.equals(mPositionOut)) {
-                updatePosition(rotation, sTmpRectF, (LayoutParams) getLayoutParams());
+            if (!sTmpRectF.equals(mPositionOut)) {
+                updatePosition(sTmpRectF, (InsettableFrameLayout.LayoutParams) getLayoutParams());
                 if (mOnTargetChangeRunnable != null) {
                     mOnTargetChangeRunnable.run();
                 }
@@ -772,6 +561,11 @@
         view.setVisibility(INVISIBLE);
         parent.addView(view);
         dragLayer.addView(view.mListenerView);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "getFloatingIconView. listenerView "
+                    + "added to dragLayer. listenerView=" + view.mListenerView + ", fiv=" + view,
+                    new Exception());
+        }
         view.mListenerView.setListener(view::fastFinish);
 
         view.mEndRunnable = () -> {
@@ -820,12 +614,6 @@
             }
         });
 
-        if (mBadge != null) {
-            ObjectAnimator badgeFade = ObjectAnimator.ofInt(mBadge, DRAWABLE_ALPHA, 255);
-            badgeFade.addUpdateListener(valueAnimator -> invalidate());
-            fade.play(badgeFade);
-        }
-
         if (originalView instanceof IconLabelDotView) {
             IconLabelDotView view = (IconLabelDotView) originalView;
             fade.addListener(new AnimatorListenerAdapter() {
@@ -857,6 +645,10 @@
     private void finish(DragLayer dragLayer) {
         ((ViewGroup) dragLayer.getParent()).removeView(this);
         dragLayer.removeView(mListenerView);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "listenerView removed from dragLayer. "
+                    + "listenerView=" + mListenerView + ", fiv=" + this, new Exception());
+        }
         recycle();
         mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
     }
@@ -867,21 +659,12 @@
         setScaleX(1);
         setScaleY(1);
         setAlpha(1);
-        setBackground(null);
         if (mLoadIconSignal != null) {
             mLoadIconSignal.cancel();
         }
         mLoadIconSignal = null;
         mEndRunnable = null;
-        mIsAdaptiveIcon = false;
-        mForeground = null;
-        mBackground = null;
-        mClipPath = null;
         mFinalDrawableBounds.setEmpty();
-        if (mRevealAnimator != null) {
-            mRevealAnimator.cancel();
-        }
-        mRevealAnimator = null;
         if (mFadeAnimatorSet != null) {
             mFadeAnimatorSet.cancel();
         }
@@ -890,15 +673,11 @@
         mListenerView.setListener(null);
         mOriginalIcon = null;
         mOnTargetChangeRunnable = null;
-        mTaskCornerRadius = 0;
-        mOutline.setEmpty();
-        mFgTransY = 0;
-        mFgSpringX.cancel();
-        mFgTransX = 0;
-        mFgSpringY.cancel();
         mBadge = null;
         sTmpObjArray[0] = null;
         mIconLoadResult = null;
+        mClipIconView.recycle();
+        mFastFinishRunnable = null;
     }
 
     private static class IconLoadResult {
@@ -909,7 +688,7 @@
         Runnable onIconLoaded;
         boolean isIconLoaded;
 
-        public IconLoadResult(ItemInfo itemInfo) {
+        IconLoadResult(ItemInfo itemInfo) {
             this.itemInfo = itemInfo;
         }
     }
diff --git a/src/com/android/launcher3/views/ListenerView.java b/src/com/android/launcher3/views/ListenerView.java
index 263f7c4..3ef778b 100644
--- a/src/com/android/launcher3/views/ListenerView.java
+++ b/src/com/android/launcher3/views/ListenerView.java
@@ -17,18 +17,20 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.testing.TestProtocol;
 
 /**
  * An invisible AbstractFloatingView that can run a callback when it is being closed.
  */
 public class ListenerView extends AbstractFloatingView {
 
-    public Runnable mCloseListener;
+    private Runnable mCloseListener;
 
     public ListenerView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -36,12 +38,20 @@
     }
 
     public void setListener(Runnable listener) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView setListener lv=" + this
+                    + ", listener=" + listener, new Exception());
+        }
         mCloseListener = listener;
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onAttachedToWindow lv=" + this,
+                    new Exception());
+        }
         mIsOpen = true;
     }
 
@@ -49,10 +59,19 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mIsOpen = false;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onDetachedFromView lv=" + this,
+                    new Exception());
+        }
     }
 
     @Override
     protected void handleClose(boolean animate) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView handeClose lv=" + this
+                    + ", mIsOpen=" + mIsOpen + ", mCloseListener=" + mCloseListener
+                    + ", getParent()=" + getParent(), new Exception());
+        }
         if (mIsOpen) {
             if (mCloseListener != null) {
                 mCloseListener.run();
@@ -77,10 +96,19 @@
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView touchEvent lv=" + this
+                    + ", ev=" + ev, new Exception());
+        }
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             handleClose(false);
         }
         // We want other views to be able to intercept the touch so we return false here.
         return false;
     }
+
+    @Override
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return true;
+    }
 }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 465df44..d558781 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -17,6 +17,9 @@
 
 import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_FLAVOR;
 import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_OFFSET;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS;
 
 import android.content.Context;
 import android.content.Intent;
@@ -35,19 +38,21 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.StatsLogManager.EventEnum;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.widget.WidgetsFullSheet;
 
 import java.util.ArrayList;
 import java.util.List;
 
-
 /**
  * Popup shown on long pressing an empty space in launcher
  */
@@ -67,21 +72,21 @@
 
     @Override
     public void onClick(View view) {
-        handleViewClick(view, Action.Touch.TAP);
+        handleViewClick(view);
     }
 
     @Override
     public boolean onLongClick(View view) {
-        return handleViewClick(view, Action.Touch.LONGPRESS);
+        return handleViewClick(view);
     }
 
-    private boolean handleViewClick(View view, int action) {
+    private boolean handleViewClick(View view) {
         OptionItem item = mItemMap.get(view);
         if (item == null) {
             return false;
         }
-        if (item.mControlTypeForLog > 0) {
-            logTap(action, item.mControlTypeForLog);
+        if (item.mEventId.getId() > 0) {
+            mLauncher.getStatsLogManager().logger().log(item.mEventId);
         }
         if (item.mClickListener.onLongClick(view)) {
             close(true);
@@ -90,10 +95,6 @@
         return false;
     }
 
-    private void logTap(int action, int controlType) {
-        mLauncher.getUserEventDispatcher().logActionOnControl(action, controlType);
-    }
-
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() != MotionEvent.ACTION_DOWN) {
@@ -127,7 +128,8 @@
         popup.mTargetRect = targetRect;
 
         for (OptionItem item : items) {
-            DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
+            DeepShortcutView view =
+                    (DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
             view.getIconView().setBackgroundResource(item.mIconRes);
             view.getBubbleText().setText(item.mLabelRes);
             view.setDividerVisibility(View.INVISIBLE);
@@ -157,13 +159,16 @@
         int resDrawable = Utilities.existsStyleWallpapers(launcher) ?
                 R.drawable.ic_palette : R.drawable.ic_wallpaper;
         options.add(new OptionItem(resString, resDrawable,
-                ControlType.WALLPAPER_BUTTON, OptionsPopupView::startWallpaperPicker));
-        if (!FeatureFlags.GO_DISABLE_WIDGETS) {
+                IGNORE,
+                OptionsPopupView::startWallpaperPicker));
+        if (!WidgetsModel.GO_DISABLE_WIDGETS) {
             options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
-                    ControlType.WIDGETS_BUTTON, OptionsPopupView::onWidgetsClicked));
+                    LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
+                    OptionsPopupView::onWidgetsClicked));
         }
         options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
-                ControlType.SETTINGS_BUTTON, OptionsPopupView::startSettings));
+                LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
+                OptionsPopupView::startSettings));
 
         show(launcher, target, options);
     }
@@ -184,6 +189,7 @@
     }
 
     public static boolean startSettings(View view) {
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startSettings");
         Launcher launcher = Launcher.getLauncher(view.getContext());
         launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
                 .setPackage(launcher.getPackageName())
@@ -214,21 +220,29 @@
         if (!TextUtils.isEmpty(pickerPackage)) {
             intent.setPackage(pickerPackage);
         }
-        return launcher.startActivitySafely(v, intent, null, null);
+        return launcher.startActivitySafely(v, intent, dummyInfo(intent), null);
+    }
+
+    static WorkspaceItemInfo dummyInfo(Intent intent) {
+        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
+        dummyInfo.intent = intent;
+        dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+        dummyInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
+        return dummyInfo;
     }
 
     public static class OptionItem {
 
         private final int mLabelRes;
         private final int mIconRes;
-        private final int mControlTypeForLog;
+        private final EventEnum mEventId;
         private final OnLongClickListener mClickListener;
 
-        public OptionItem(int labelRes, int iconRes, int controlTypeForLog,
+        public OptionItem(int labelRes, int iconRes, EventEnum eventId,
                 OnLongClickListener clickListener) {
             mLabelRes = labelRes;
             mIconRes = iconRes;
-            mControlTypeForLog = controlTypeForLog;
+            mEventId = eventId;
             mClickListener = clickListener;
         }
     }
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 5653801..6a83332 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -67,7 +67,6 @@
 
     private final static int MAX_TRACK_ALPHA = 30;
     private final static int SCROLL_BAR_VIS_DURATION = 150;
-    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;
 
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
@@ -184,7 +183,7 @@
         if (mThumbOffsetY == y) {
             return;
         }
-        updatePopupY((int) y);
+        updatePopupY(y);
         mThumbOffsetY = y;
         invalidate();
     }
@@ -237,7 +236,7 @@
                 } else if (mRv.supportsFastScrolling()
                         && isNearScrollBar(mDownX)) {
                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
-                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
+                    updateFastScrollSectionNameAndThumbOffset(y);
                 }
                 break;
             case MotionEvent.ACTION_MOVE:
@@ -252,7 +251,7 @@
                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
                 }
                 if (mIsDragging) {
-                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
+                    updateFastScrollSectionNameAndThumbOffset(y);
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -281,7 +280,7 @@
         showActiveScrollbar(true);
     }
 
-    private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
+    private void updateFastScrollSectionNameAndThumbOffset(int y) {
         // Update the fastscroller section name at this touch position
         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 9f59d78..22faf97 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -22,9 +22,12 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -33,14 +36,16 @@
 import android.animation.PropertyValuesHolder;
 import android.animation.RectEvaluator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.util.AttributeSet;
-import android.util.Property;
+import android.util.IntProperty;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -49,6 +54,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
@@ -58,10 +64,10 @@
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
@@ -73,15 +79,14 @@
 
 import java.util.List;
 
-
 /**
  * Simple scrim which draws a flat color
  */
-public class ScrimView extends View implements Insettable, OnChangeListener,
-        AccessibilityStateChangeListener, StateListener {
+public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
+        AccessibilityStateChangeListener {
 
-    public static final Property<ScrimView, Integer> DRAG_HANDLE_ALPHA =
-            new Property<ScrimView, Integer>(Integer.TYPE, "dragHandleAlpha") {
+    public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
+            new IntProperty<ScrimView>("dragHandleAlpha") {
 
                 @Override
                 public Integer get(ScrimView scrimView) {
@@ -89,7 +94,7 @@
                 }
 
                 @Override
-                public void set(ScrimView scrimView, Integer value) {
+                public void setValue(ScrimView scrimView, int value) {
                     scrimView.setDragHandleAlpha(value);
                 }
             };
@@ -98,13 +103,30 @@
     private static final int SETTINGS = R.string.settings_button_text;
     private static final int ALPHA_CHANNEL_COUNT = 1;
 
+    private static final long DRAG_HANDLE_BOUNCE_DURATION_MS = 300;
+    // How much to delay before repeating the bounce.
+    private static final long DRAG_HANDLE_BOUNCE_DELAY_MS = 200;
+    // Repeat this many times (i.e. total number of bounces is 1 + this).
+    private static final int DRAG_HANDLE_BOUNCE_REPEAT_COUNT = 2;
+
     private final Rect mTempRect = new Rect();
     private final int[] mTempPos = new int[2];
 
-    protected final Launcher mLauncher;
+    protected final T mLauncher;
     private final WallpaperColorInfo mWallpaperColorInfo;
     private final AccessibilityManager mAM;
     protected final int mEndScrim;
+    protected final boolean mIsScrimDark;
+
+    private final StateListener<LauncherState> mAccessibilityLauncherStateListener =
+            new StateListener<LauncherState>() {
+        @Override
+        public void onStateTransitionComplete(LauncherState finalState) {
+            setImportantForAccessibility(finalState == ALL_APPS
+                    ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                    : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        }
+    };
 
     protected float mMaxScrimAlpha;
 
@@ -115,10 +137,13 @@
     protected int mEndFlatColor;
     protected int mEndFlatColorAlpha;
 
-    protected final int mDragHandleSize;
+    protected final Point mDragHandleSize;
+    private final int mDragHandleTouchSize;
+    private final int mDragHandlePaddingInVerticalBarLayout;
     protected float mDragHandleOffset;
     private final Rect mDragHandleBounds;
     private final RectF mHitRect = new RectF();
+    private ObjectAnimator mDragHandleAnim;
 
     private final MultiValueAlpha mMultiValueAlpha;
 
@@ -130,15 +155,20 @@
 
     public ScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mLauncher = Launcher.getLauncher(context);
-        mWallpaperColorInfo = WallpaperColorInfo.getInstance(context);
+        mLauncher = Launcher.cast(Launcher.getLauncher(context));
+        mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
         mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
+        mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
 
         mMaxScrimAlpha = 0.7f;
 
-        mDragHandleSize = context.getResources()
-                .getDimensionPixelSize(R.dimen.vertical_drag_handle_size);
-        mDragHandleBounds = new Rect(0, 0, mDragHandleSize, mDragHandleSize);
+        Resources res = context.getResources();
+        mDragHandleSize = new Point(res.getDimensionPixelSize(R.dimen.vertical_drag_handle_width),
+                res.getDimensionPixelSize(R.dimen.vertical_drag_handle_height));
+        mDragHandleBounds = new Rect(0, 0, mDragHandleSize.x, mDragHandleSize.y);
+        mDragHandleTouchSize = res.getDimensionPixelSize(R.dimen.vertical_drag_handle_touch_size);
+        mDragHandlePaddingInVerticalBarLayout = context.getResources()
+                .getDimensionPixelSize(R.dimen.vertical_drag_handle_padding_in_vertical_bar_layout);
 
         mAccessibilityHelper = createAccessibilityHelper();
         ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
@@ -160,7 +190,7 @@
     @Override
     public void setInsets(Rect insets) {
         updateDragHandleBounds();
-        updateDragHandleVisibility(null);
+        updateDragHandleVisibility();
     }
 
     @Override
@@ -204,7 +234,9 @@
     public void setProgress(float progress) {
         if (mProgress != progress) {
             mProgress = progress;
+            stopDragHandleEducationAnim();
             updateColors();
+            updateSysUiColors();
             updateDragHandleAlpha();
             invalidate();
         }
@@ -217,6 +249,17 @@
                 mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
     }
 
+    protected void updateSysUiColors() {
+        // Use a light system UI (dark icons) if all apps is behind at least half of the
+        // status bar.
+        boolean forceChange = mProgress <= 0.1f;
+        if (forceChange) {
+            mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
+        } else {
+            mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
+        }
+    }
+
     protected void updateDragHandleAlpha() {
         if (mDragHandle != null) {
             mDragHandle.setAlpha(mDragHandleAlpha);
@@ -251,70 +294,103 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        boolean value = super.onTouchEvent(event);
-        if (!value && mDragHandle != null && event.getAction() == ACTION_DOWN
-                && mDragHandle.getAlpha() == 255
-                && mHitRect.contains(event.getX(), event.getY())) {
-
-            final Drawable drawable = mDragHandle;
-            mDragHandle = null;
-
-            Rect bounds = new Rect(mDragHandleBounds);
-            bounds.offset(0, -(int) mDragHandleOffset);
-            drawable.setBounds(bounds);
-
-            Rect topBounds = new Rect(bounds);
-            topBounds.offset(0, -bounds.height() / 2);
-
-            Rect invalidateRegion = new Rect(bounds);
-            invalidateRegion.top = topBounds.top;
-
-            Keyframe frameTop = Keyframe.ofObject(0.6f, topBounds);
-            frameTop.setInterpolator(DEACCEL);
-            Keyframe frameBot = Keyframe.ofObject(1, bounds);
-            frameBot.setInterpolator(ACCEL);
-            PropertyValuesHolder holder = PropertyValuesHolder .ofKeyframe("bounds",
-                    Keyframe.ofObject(0, bounds), frameTop, frameBot);
-            holder.setEvaluator(new RectEvaluator());
-
-            ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    getOverlay().remove(drawable);
-                    updateDragHandleVisibility(drawable);
+        boolean superHandledTouch = super.onTouchEvent(event);
+        if (event.getAction() == ACTION_DOWN) {
+            if (!superHandledTouch && mHitRect.contains(event.getX(), event.getY())) {
+                if (startDragHandleEducationAnim()) {
+                    return true;
                 }
-            });
-            anim.addUpdateListener((v) -> invalidate(invalidateRegion));
-            getOverlay().add(drawable);
-            anim.start();
-            return true;
+            }
+            stopDragHandleEducationAnim();
         }
-        return value;
+        return superHandledTouch;
+    }
+
+    /**
+     * Animates the drag handle to demonstrate how to get to all apps.
+     * @return Whether the animation was started (false if drag handle is invisible).
+     */
+    public boolean startDragHandleEducationAnim() {
+        stopDragHandleEducationAnim();
+
+        if (mDragHandle == null || mDragHandle.getAlpha() != 255) {
+            return false;
+        }
+
+        final Drawable drawable = mDragHandle;
+        mDragHandle = null;
+
+        Rect bounds = new Rect(mDragHandleBounds);
+        bounds.offset(0, -(int) mDragHandleOffset);
+        drawable.setBounds(bounds);
+
+        Rect topBounds = new Rect(bounds);
+        topBounds.offset(0, -bounds.height());
+
+        Rect invalidateRegion = new Rect(bounds);
+        invalidateRegion.top = topBounds.top;
+
+        final float progressToReachTop = 0.6f;
+        Keyframe frameTop = Keyframe.ofObject(progressToReachTop, topBounds);
+        frameTop.setInterpolator(DEACCEL);
+        Keyframe frameBot = Keyframe.ofObject(1, bounds);
+        frameBot.setInterpolator(ACCEL_DEACCEL);
+        PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("bounds",
+                Keyframe.ofObject(0, bounds), frameTop, frameBot);
+        holder.setEvaluator(new RectEvaluator());
+
+        mDragHandleAnim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
+        long totalBounceDuration = DRAG_HANDLE_BOUNCE_DURATION_MS + DRAG_HANDLE_BOUNCE_DELAY_MS;
+        // The bounce finishes by this progress, the rest of the duration just delays next bounce.
+        float delayStartProgress = 1f - (float) DRAG_HANDLE_BOUNCE_DELAY_MS / totalBounceDuration;
+        mDragHandleAnim.addUpdateListener((v) -> invalidate(invalidateRegion));
+        mDragHandleAnim.setDuration(totalBounceDuration);
+        mDragHandleAnim.setInterpolator(clampToProgress(LINEAR, 0, delayStartProgress));
+        mDragHandleAnim.setRepeatCount(DRAG_HANDLE_BOUNCE_REPEAT_COUNT);
+        getOverlay().add(drawable);
+
+        mDragHandleAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mDragHandleAnim = null;
+                getOverlay().remove(drawable);
+                updateDragHandleVisibility(drawable);
+            }
+        });
+        mDragHandleAnim.start();
+        return true;
+    }
+
+    private void stopDragHandleEducationAnim() {
+        if (mDragHandleAnim != null) {
+            mDragHandleAnim.end();
+        }
     }
 
     protected void updateDragHandleBounds() {
         DeviceProfile grid = mLauncher.getDeviceProfile();
         final int left;
         final int width = getMeasuredWidth();
-        final int top = getMeasuredHeight() - mDragHandleSize - grid.getInsets().bottom;
+        final int top = getMeasuredHeight() - mDragHandleSize.y - grid.getInsets().bottom;
         final int topMargin;
 
         if (grid.isVerticalBarLayout()) {
-            topMargin = grid.workspacePadding.bottom;
+            topMargin = grid.workspacePadding.bottom + mDragHandlePaddingInVerticalBarLayout;
             if (grid.isSeascape()) {
-                left = width - grid.getInsets().right - mDragHandleSize;
+                left = width - grid.getInsets().right - mDragHandleSize.x
+                        - mDragHandlePaddingInVerticalBarLayout;
             } else {
-                left = mDragHandleSize + grid.getInsets().left;
+                left = grid.getInsets().left + mDragHandlePaddingInVerticalBarLayout;
             }
         } else {
-            left = (width - mDragHandleSize) / 2;
+            left = Math.round((width - mDragHandleSize.x) / 2f);
             topMargin = grid.hotseatBarSizePx;
         }
         mDragHandleBounds.offsetTo(left, top - topMargin);
         mHitRect.set(mDragHandleBounds);
-        float inset = -mDragHandleSize / 2;
-        mHitRect.inset(inset, inset);
+        // Inset outwards to increase touch size.
+        mHitRect.inset((mDragHandleSize.x - mDragHandleTouchSize) / 2f,
+                (mDragHandleSize.y - mDragHandleTouchSize) / 2f);
 
         if (mDragHandle != null) {
             mDragHandle.setBounds(mDragHandleBounds);
@@ -323,25 +399,29 @@
 
     @Override
     public void onAccessibilityStateChanged(boolean enabled) {
-        LauncherStateManager stateManager = mLauncher.getStateManager();
-        stateManager.removeStateListener(this);
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        stateManager.removeStateListener(mAccessibilityLauncherStateListener);
 
         if (enabled) {
-            stateManager.addStateListener(this);
-            handleStateChangedComplete(stateManager.getState());
+            stateManager.addStateListener(mAccessibilityLauncherStateListener);
+            mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
         } else {
             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
         }
+        updateDragHandleVisibility();
+    }
+
+    public void updateDragHandleVisibility() {
         updateDragHandleVisibility(null);
     }
 
-    private void updateDragHandleVisibility(Drawable recycle) {
-        boolean visible = mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
+    private void updateDragHandleVisibility(@Nullable Drawable recycle) {
+        boolean visible = shouldDragHandleBeVisible();
         boolean wasVisible = mDragHandle != null;
         if (visible != wasVisible) {
             if (visible) {
                 mDragHandle = recycle != null ? recycle :
-                        mLauncher.getDrawable(R.drawable.drag_handle_indicator);
+                        mLauncher.getDrawable(R.drawable.drag_handle_indicator_shadow);
                 mDragHandle.setBounds(mDragHandleBounds);
 
                 updateDragHandleAlpha();
@@ -352,6 +432,10 @@
         }
     }
 
+    protected boolean shouldDragHandleBeVisible() {
+        return mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
+    }
+
     @Override
     public boolean dispatchHoverEvent(MotionEvent event) {
         return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
@@ -369,20 +453,6 @@
         mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
     }
 
-    @Override
-    public void onStateTransitionStart(LauncherState toState) {}
-
-    @Override
-    public void onStateTransitionComplete(LauncherState finalState) {
-        handleStateChangedComplete(finalState);
-    }
-
-    private void handleStateChangedComplete(LauncherState finalState) {
-        setImportantForAccessibility(finalState == ALL_APPS
-                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-    }
-
     protected class AccessibilityHelper extends ExploreByTouchHelper {
 
         private static final int DRAG_HANDLE_ID = 1;
@@ -393,7 +463,7 @@
 
         @Override
         protected int getVirtualViewAt(float x, float y) {
-            return  mDragHandleBounds.contains((int) x, (int) y)
+            return  mHitRect.contains((int) x, (int) y)
                     ? DRAG_HANDLE_ID : INVALID_ID;
         }
 
@@ -466,7 +536,10 @@
         }
     }
 
-    public int getDragHandleSize() {
-        return mDragHandleSize;
+    /**
+     * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
+     */
+    public float getVisualTop() {
+        return Float.MAX_VALUE;
     }
 }
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index dc0e2e0..513ce59 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -29,7 +29,7 @@
 import android.widget.TextView;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -44,7 +44,7 @@
     private static final long HIDE_DURATION_MS = 180;
     private static final int TIMEOUT_DURATION_MS = 4000;
 
-    private final Launcher mLauncher;
+    private final BaseDraggingActivity mActivity;
     private Runnable mOnDismissed;
 
     public Snackbar(Context context, AttributeSet attrs) {
@@ -53,25 +53,25 @@
 
     public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
+        mActivity = BaseDraggingActivity.fromContext(context);
         inflate(context, R.layout.snackbar, this);
     }
 
-    public static void show(Launcher launcher, int labelStringResId, int actionStringResId,
-            Runnable onDismissed, Runnable onActionClicked) {
-        closeOpenViews(launcher, true, TYPE_SNACKBAR);
-        Snackbar snackbar = new Snackbar(launcher, null);
+    public static void show(BaseDraggingActivity activity, int labelStringResId,
+            int actionStringResId, Runnable onDismissed, Runnable onActionClicked) {
+        closeOpenViews(activity, true, TYPE_SNACKBAR);
+        Snackbar snackbar = new Snackbar(activity, null);
         // Set some properties here since inflated xml only contains the children.
         snackbar.setOrientation(HORIZONTAL);
         snackbar.setGravity(Gravity.CENTER_VERTICAL);
-        Resources res = launcher.getResources();
+        Resources res = activity.getResources();
         snackbar.setElevation(res.getDimension(R.dimen.snackbar_elevation));
         int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding);
         snackbar.setPadding(padding, padding, padding, padding);
         snackbar.setBackgroundResource(R.drawable.round_rect_primary);
 
         snackbar.mIsOpen = true;
-        DragLayer dragLayer = launcher.getDragLayer();
+        BaseDragLayer dragLayer = activity.getDragLayer();
         dragLayer.addView(snackbar);
 
         DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams();
@@ -80,7 +80,7 @@
         int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right);
         int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right);
         int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom);
-        Rect insets = launcher.getDeviceProfile().getInsets();
+        Rect insets = activity.getDeviceProfile().getInsets();
         int maxWidth = dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right;
         int minWidth = dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right;
         params.width = minWidth;
@@ -135,7 +135,7 @@
                 .setDuration(SHOW_DURATION_MS)
                 .setInterpolator(Interpolators.ACCEL_DEACCEL)
                 .start();
-        int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(launcher,
+        int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(activity,
                 TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS);
         snackbar.postDelayed(() -> snackbar.close(true), timeout);
     }
@@ -160,7 +160,7 @@
     }
 
     private void onClosed() {
-        mLauncher.getDragLayer().removeView(this);
+        mActivity.getDragLayer().removeView(this);
         if (mOnDismissed != null) {
             mOnDismissed.run();
         }
@@ -179,7 +179,7 @@
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dl = mLauncher.getDragLayer();
+            BaseDragLayer dl = mActivity.getDragLayer();
             if (!dl.isEventOverView(this, ev)) {
                 close(true);
             }
diff --git a/src/com/android/launcher3/views/WorkEduView.java b/src/com/android/launcher3/views/WorkEduView.java
new file mode 100644
index 0000000..d35a38f
--- /dev/null
+++ b/src/com/android/launcher3/views/WorkEduView.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+
+/**
+ * On boarding flow for users right after setting up work profile
+ */
+public class WorkEduView extends AbstractSlideInView
+        implements Insettable, StateListener<LauncherState> {
+
+    private static final int DEFAULT_CLOSE_DURATION = 200;
+    public static final String KEY_WORK_EDU_STEP = "showed_work_profile_edu";
+    public static final String KEY_LEGACY_WORK_EDU_SEEN = "showed_bottom_user_education";
+
+    private static final int WORK_EDU_NOT_STARTED = 0;
+    private static final int WORK_EDU_PERSONAL_APPS = 1;
+    private static final int WORK_EDU_WORK_APPS = 2;
+
+    protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
+
+
+    private Rect mInsets = new Rect();
+    private View mViewWrapper;
+    private Button mProceedButton;
+    private TextView mContentText;
+    private AllAppsPagedView mAllAppsPagedView;
+
+    private int mNextWorkEduStep = WORK_EDU_PERSONAL_APPS;
+
+
+    public WorkEduView(Context context, AttributeSet attr) {
+        this(context, attr, 0);
+    }
+
+    public WorkEduView(Context context, AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContent = this;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        mLauncher.getSharedPrefs().edit().putInt(KEY_WORK_EDU_STEP, mNextWorkEduStep).apply();
+        handleClose(true, DEFAULT_CLOSE_DURATION);
+    }
+
+    @Override
+    protected void onCloseComplete() {
+        super.onCloseComplete();
+        mLauncher.getStateManager().removeStateListener(this);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // Since this is on-boarding popup, it is not a user controlled action.
+    }
+
+    @Override
+    public int getLogContainerType() {
+        return LauncherLogProto.ContainerType.TIP;
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mViewWrapper = findViewById(R.id.view_wrapper);
+        mProceedButton = findViewById(R.id.proceed);
+        mContentText = findViewById(R.id.content_text);
+
+        // make sure layout does not shrink when we change the text
+        mContentText.post(() -> mContentText.setMinLines(mContentText.getLineCount()));
+        if (mLauncher.getAppsView().getContentView() instanceof AllAppsPagedView) {
+            mAllAppsPagedView = (AllAppsPagedView) mLauncher.getAppsView().getContentView();
+        }
+
+        mProceedButton.setOnClickListener(view -> {
+            if (mAllAppsPagedView != null) {
+                mAllAppsPagedView.snapToPage(AllAppsContainerView.AdapterHolder.WORK);
+            }
+            goToWorkTab(true);
+        });
+    }
+
+    private void goToWorkTab(boolean animate) {
+        mProceedButton.setText(R.string.work_profile_edu_accept);
+        if (animate) {
+            ObjectAnimator animator = ObjectAnimator.ofFloat(mContentText, ALPHA, 0);
+            animator.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    mContentText.setText(mLauncher.getString(R.string.work_profile_edu_work_apps));
+                    ObjectAnimator.ofFloat(mContentText, ALPHA, 1).start();
+                }
+            });
+            animator.start();
+        } else {
+            mContentText.setText(mLauncher.getString(R.string.work_profile_edu_work_apps));
+        }
+        mNextWorkEduStep = WORK_EDU_WORK_APPS;
+        mProceedButton.setOnClickListener(v -> handleClose(true));
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        int leftInset = insets.left - mInsets.left;
+        int rightInset = insets.right - mInsets.right;
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        setPadding(leftInset, getPaddingTop(), rightInset, 0);
+        mViewWrapper.setPaddingRelative(mViewWrapper.getPaddingStart(),
+                mViewWrapper.getPaddingTop(), mViewWrapper.getPaddingEnd(), bottomInset);
+    }
+
+    private void show() {
+        attachToContainer();
+        animateOpen();
+        mLauncher.getStateManager().addStateListener(this);
+    }
+
+    @Override
+    protected int getScrimColor(Context context) {
+        return FINAL_SCRIM_BG_COLOR;
+    }
+
+    private void goToFirstPage() {
+        if (mAllAppsPagedView != null) {
+            mAllAppsPagedView.snapToPageImmediately(AllAppsContainerView.AdapterHolder.MAIN);
+        }
+    }
+
+    private void animateOpen() {
+        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+            return;
+        }
+        mIsOpen = true;
+        mOpenCloseAnimator.setValues(
+                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mOpenCloseAnimator.start();
+    }
+
+    /**
+     * Checks if user has not seen onboarding UI yet and shows it when user navigates to all apps
+     */
+    public static StateListener<LauncherState> showEduFlowIfNeeded(Launcher launcher,
+            @Nullable StateListener<LauncherState> oldListener) {
+        if (oldListener != null) {
+            launcher.getStateManager().removeStateListener(oldListener);
+        }
+        if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
+                WORK_EDU_NOT_STARTED) != WORK_EDU_NOT_STARTED) {
+            return null;
+        }
+
+        StateListener<LauncherState> listener = new StateListener<LauncherState>() {
+            @Override
+            public void onStateTransitionComplete(LauncherState finalState) {
+                if (finalState != LauncherState.ALL_APPS) return;
+                LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+                WorkEduView v = (WorkEduView) layoutInflater.inflate(
+                        R.layout.work_profile_edu, launcher.getDragLayer(),
+                        false);
+                v.show();
+                v.goToFirstPage();
+                launcher.getStateManager().removeStateListener(this);
+            }
+        };
+        launcher.getStateManager().addStateListener(listener);
+        return listener;
+    }
+
+    /**
+     * Shows work apps edu if user had dismissed full edu flow
+     */
+    public static void showWorkEduIfNeeded(Launcher launcher) {
+        if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
+                WORK_EDU_NOT_STARTED) != WORK_EDU_PERSONAL_APPS) {
+            return;
+        }
+        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+        WorkEduView v = (WorkEduView) layoutInflater.inflate(
+                R.layout.work_profile_edu, launcher.getDragLayer(), false);
+        v.show();
+        v.goToWorkTab(false);
+    }
+
+    private static boolean hasSeenLegacyEdu(Launcher launcher) {
+        return launcher.getSharedPrefs().getBoolean(KEY_LEGACY_WORK_EDU_SEEN, false);
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        close(false);
+    }
+}
diff --git a/src/com/android/launcher3/views/WorkFooterContainer.java b/src/com/android/launcher3/views/WorkFooterContainer.java
deleted file mode 100644
index fb17b4f..0000000
--- a/src/com/android/launcher3/views/WorkFooterContainer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.views;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.RelativeLayout;
-
-/**
- * Container to show work footer in all-apps.
- */
-public class WorkFooterContainer extends RelativeLayout {
-
-    public WorkFooterContainer(Context context) {
-        super(context);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        updateTranslation();
-    }
-
-    @Override
-    public void offsetTopAndBottom(int offset) {
-        super.offsetTopAndBottom(offset);
-        updateTranslation();
-    }
-
-    private void updateTranslation() {
-        if (getParent() instanceof View) {
-            View parent = (View) getParent();
-            int availableBot = parent.getHeight() - parent.getPaddingBottom();
-            setTranslationY(Math.max(0, availableBot - getBottom()));
-        }
-    }
-}
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 72cddc7..3e5113a 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.widget;
 
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 
@@ -30,11 +28,13 @@
 
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -42,7 +42,8 @@
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.AbstractSlideInView;
-import com.android.launcher3.views.BaseDragLayer;
+
+import java.util.ArrayList;
 
 /**
  * Base class for various widgets popup
@@ -55,11 +56,14 @@
     /* Touch handling related member variables. */
     private Toast mWidgetInstructionToast;
 
-    protected final View mColorScrim;
-
     public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mColorScrim = createColorScrim(context);
+    }
+
+    protected int getScrimColor(Context context) {
+        WallpaperColorInfo colors = WallpaperColorInfo.INSTANCE.get(context);
+        int alpha = context.getResources().getInteger(R.integer.extracted_color_gradient_alpha);
+        return setColorAlphaBound(colors.getSecondaryColor(), alpha);
     }
 
     @Override
@@ -90,6 +94,8 @@
 
     @Override
     public boolean onLongClick(View v) {
+        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
+        v.cancelLongPress();
         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
 
         if (v instanceof WidgetCell) {
@@ -98,16 +104,6 @@
         return true;
     }
 
-    protected void attachToContainer() {
-        getPopupContainer().addView(mColorScrim);
-        getPopupContainer().addView(this);
-    }
-
-    protected void setTranslationShift(float translationShift) {
-        super.setTranslationShift(translationShift);
-        mColorScrim.setAlpha(1 - mTranslationShift);
-    }
-
     private boolean beginDraggingWidget(WidgetCell v) {
         // Get the widget preview as the drag representation
         WidgetImageView image = v.getWidgetView();
@@ -138,7 +134,6 @@
 
     protected void onCloseComplete() {
         super.onCloseComplete();
-        getPopupContainer().removeView(mColorScrim);
         clearNavBarColor();
     }
 
@@ -155,9 +150,11 @@
     }
 
     @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        targetParent.containerType = ContainerType.WIDGETS;
-        targetParent.cardinality = getElementsRowCount();
+    public void fillInLogContainerData(ItemInfo childInfo, Target child,
+            ArrayList<Target> parents) {
+        Target target = newContainerTarget(ContainerType.WIDGETS);
+        target.cardinality = getElementsRowCount();
+        parents.add(target);
     }
 
     @Override
@@ -177,19 +174,4 @@
     protected SystemUiController getSystemUiController() {
         return mLauncher.getSystemUiController();
     }
-
-    private static View createColorScrim(Context context) {
-        View view = new View(context);
-        view.forceHasOverlappingRendering(false);
-
-        WallpaperColorInfo colors = WallpaperColorInfo.getInstance(context);
-        int alpha = context.getResources().getInteger(R.integer.extracted_color_gradient_alpha);
-        view.setBackgroundColor(setColorAlphaBound(colors.getSecondaryColor(), alpha));
-
-        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
-        lp.ignoreInsets = true;
-        view.setLayoutParams(lp);
-
-        return view;
-    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index f3fd7ca..6f2e179 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,14 +19,12 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.PointF;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -35,15 +33,13 @@
 import android.widget.RemoteViews;
 
 import com.android.launcher3.CheckLongPressHelper;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.SimpleOnStylusPressListener;
-import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
@@ -64,34 +60,22 @@
     protected final LayoutInflater mInflater;
 
     private final CheckLongPressHelper mLongPressHelper;
-    private final StylusEventHelper mStylusEventHelper;
     protected final Launcher mLauncher;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mReinflateOnConfigChange;
 
-    private float mSlop;
-
     private boolean mIsScrollable;
     private boolean mIsAttachedToWindow;
     private boolean mIsAutoAdvanceRegistered;
     private Runnable mAutoAdvanceRunnable;
 
-    /**
-     * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
-     */
-    private float mScaleToFit = 1f;
 
-    /**
-     * The translation values to center the widget within its cellspans.
-     */
-    private final PointF mTranslationForCentering = new PointF(0, 0);
 
     public LauncherAppWidgetHostView(Context context) {
         super(context);
         mLauncher = Launcher.getLauncher(context);
         mLongPressHelper = new CheckLongPressHelper(this, this);
-        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
         mInflater = LayoutInflater.from(context);
         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
@@ -155,68 +139,19 @@
     }
 
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        // Just in case the previous long press hasn't been cleared, we make sure to start fresh
-        // on touch down.
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mLongPressHelper.cancelLongPress();
-        }
-
-        // Consume any touch events for ourselves after longpress is triggered
-        if (mLongPressHelper.hasPerformedLongPress()) {
-            mLongPressHelper.cancelLongPress();
-            return true;
-        }
-
-        // Watch for longpress or stylus button press events at this level to
-        // make sure users can always pick up this widget
-        if (mStylusEventHelper.onMotionEvent(ev)) {
-            mLongPressHelper.cancelLongPress();
-            return true;
-        }
-
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN: {
-                DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
-
-                if (mIsScrollable) {
-                     dragLayer.requestDisallowInterceptTouchEvent(true);
-                }
-                if (!mStylusEventHelper.inStylusButtonPressed()) {
-                    mLongPressHelper.postCheckForLongPress();
-                }
-                dragLayer.setTouchCompleteListener(this);
-                break;
+            DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
+            if (mIsScrollable) {
+                dragLayer.requestDisallowInterceptTouchEvent(true);
             }
-
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                mLongPressHelper.cancelLongPress();
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
-                    mLongPressHelper.cancelLongPress();
-                }
-                break;
+            dragLayer.setTouchCompleteListener(this);
         }
-
-        // Otherwise continue letting touch events fall through to children
-        return false;
+        mLongPressHelper.onTouchEvent(ev);
+        return mLongPressHelper.hasPerformedLongPress();
     }
 
     public boolean onTouchEvent(MotionEvent ev) {
-        // If the widget does not handle touch, then cancel
-        // long press when we release the touch
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                mLongPressHelper.cancelLongPress();
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) {
-                    mLongPressHelper.cancelLongPress();
-                }
-                break;
-        }
+        mLongPressHelper.onTouchEvent(ev);
         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
         return true;
     }
@@ -224,7 +159,6 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
 
         mIsAttachedToWindow = true;
         checkIfAutoAdvance();
@@ -362,26 +296,6 @@
         scheduleNextAdvance();
     }
 
-    public void setScaleToFit(float scale) {
-        mScaleToFit = scale;
-        setScaleX(scale);
-        setScaleY(scale);
-    }
-
-    public float getScaleToFit() {
-        return mScaleToFit;
-    }
-
-    public void setTranslationForCentering(float x, float y) {
-        mTranslationForCentering.set(x, y);
-        setTranslationX(x);
-        setTranslationY(y);
-    }
-
-    public PointF getTranslationForCentering() {
-        return mTranslationForCentering;
-    }
-
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 68b1595..ed42bc4 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -18,18 +18,37 @@
 
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 
+import com.android.launcher3.Reorderable;
+import com.android.launcher3.dragndrop.DraggableView;
+
 import java.util.ArrayList;
 
 /**
  * Extension of AppWidgetHostView with support for controlled keyboard navigation.
  */
-public abstract class NavigableAppWidgetHostView extends AppWidgetHostView {
+public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
+        implements DraggableView, Reorderable {
+
+    /**
+     * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
+     */
+    private float mScaleToFit = 1f;
+
+    /**
+     * The translation values to center the widget within its cellspans.
+     */
+    private final PointF mTranslationForCentering = new PointF(0, 0);
+
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+    private float mScaleForReorderBounce = 1f;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mChildrenFocused;
@@ -133,4 +152,76 @@
         // The host view's background changes when selected, to indicate the focus is inside.
         setSelected(childIsFocused);
     }
+
+    public View getView() {
+        return this;
+    }
+
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x
+                + mTranslationForCentering.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y
+                + mTranslationForCentering.y);
+    }
+
+    public void setTranslationForCentering(float x, float y) {
+        mTranslationForCentering.set(x, y);
+        updateTranslation();
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
+    }
+
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
+    }
+
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    private void updateScale() {
+        super.setScaleX(mScaleToFit * mScaleForReorderBounce);
+        super.setScaleY(mScaleToFit * mScaleForReorderBounce);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
+        updateScale();
+    }
+
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public void setScaleToFit(float scale) {
+        mScaleToFit = scale;
+        updateScale();
+    }
+
+    public float getScaleToFit() {
+        return mScaleToFit;
+    }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_WIDGET;
+    }
+
+    @Override
+    public void getWorkspaceVisualDragBounds(Rect bounds) {
+        int width = (int) (getMeasuredWidth() * mScaleToFit);
+        int height = (int) (getMeasuredHeight() * mScaleToFit);
+
+        bounds.set(0, 0 , width, height);
+    }
 }
diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
index 62b6903..9601652 100644
--- a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
@@ -15,8 +15,10 @@
  */
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+
 import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 
 /**
  * Meta data used for late binding of the short cuts.
@@ -32,5 +34,6 @@
         componentName = activityInfo.getComponent();
         user = activityInfo.getUser();
         itemType = activityInfo.getItemType();
+        this.container = CONTAINER_WIDGETS_TRAY;
     }
 }
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index bc40484..bef9a08 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+
 import android.appwidget.AppWidgetHostView;
 import android.os.Bundle;
 
@@ -50,6 +52,7 @@
         spanY = i.spanY;
         minSpanX = i.minSpanX;
         minSpanY = i.minSpanY;
+        this.container = CONTAINER_WIDGETS_TRAY;
     }
 
     public WidgetAddFlowHandler getHandler() {
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 50db40f..9021d9e 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -30,16 +33,16 @@
 import android.view.ContextThemeWrapper;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.RemoteViews;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.R;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.R;
-import com.android.launcher3.graphics.DrawableFactory;
-import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Themes;
 
@@ -92,6 +95,15 @@
     }
 
     @Override
+    public void updateAppWidget(RemoteViews remoteViews) {
+        super.updateAppWidget(remoteViews);
+        WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext());
+        if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) {
+            reInflate();
+        }
+    }
+
+    @Override
     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
             int maxHeight) {
         // No-op
@@ -128,24 +140,22 @@
             mCenterDrawable.setCallback(null);
             mCenterDrawable = null;
         }
-        if (info.iconBitmap != null) {
+        if (info.bitmap.icon != null) {
             // The view displays three modes,
             //   1) App icon in the center
             //   2) Preload icon in the center
             //   3) Setup icon in the center and app icon in the top right corner.
-            DrawableFactory drawableFactory = DrawableFactory.INSTANCE.get(getContext());
             if (mDisabledForSafeMode) {
-                FastBitmapDrawable disabledIcon = drawableFactory.newIcon(getContext(), info);
+                FastBitmapDrawable disabledIcon = newIcon(getContext(), info);
                 disabledIcon.setIsDisabled(true);
                 mCenterDrawable = disabledIcon;
                 mSettingIconDrawable = null;
             } else if (isReadyForClickSetup()) {
-                mCenterDrawable = drawableFactory.newIcon(getContext(), info);
+                mCenterDrawable = newIcon(getContext(), info);
                 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
-                updateSettingColor(info.iconColor);
+                updateSettingColor(info.bitmap.color);
             } else {
-                mCenterDrawable = DrawableFactory.INSTANCE.get(getContext())
-                        .newPendingIcon(getContext(), info);
+                mCenterDrawable = newPendingIcon(getContext(), info);
                 mSettingIconDrawable = null;
                 applyState();
             }
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 8ea9bd4..3c11274 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.PendingAddItemInfo;
 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;
@@ -79,6 +80,8 @@
 
         mEstimatedCellSize = launcher.getWorkspace().estimateItemSize(mAddInfo);
 
+        DraggableView draggableView;
+
         if (mAddInfo instanceof PendingAddWidgetInfo) {
             PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) mAddInfo;
 
@@ -91,8 +94,8 @@
                         createWidgetInfo.info, maxWidth, previewSizeBeforeScale);
             }
             if (preview == null) {
-                preview = app.getWidgetCache().generateWidgetPreview(
-                        launcher, createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale);
+                preview = app.getWidgetCache().generateWidgetPreview(launcher,
+                        createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale).first;
             }
 
             if (previewSizeBeforeScale[0] < previewBitmapWidth) {
@@ -110,6 +113,7 @@
 
             dragOffset = null;
             dragRegion = null;
+            draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_WIDGET);
         } else {
             PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) mAddInfo;
             Drawable icon = createShortcutInfo.activityInfo.getFullResIcon(app.getIconCache());
@@ -136,6 +140,7 @@
             dragRegion.top = (mEstimatedCellSize[1]
                     - iconSize - dp.iconTextSizePx - dp.iconDrawablePaddingPx) / 2;
             dragRegion.bottom = dragRegion.top + iconSize;
+            draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
         }
 
         // Since we are not going through the workspace for starting the drag, set drag related
@@ -148,8 +153,8 @@
                 + (int) ((scale * preview.getHeight() - preview.getHeight()) / 2);
 
         // Start the drag
-        launcher.getDragController().startDrag(preview, dragLayerX, dragLayerY, source, mAddInfo,
-                dragOffset, dragRegion, scale, scale, options);
+        launcher.getDragController().startDrag(preview, draggableView, dragLayerX, dragLayerY,
+                source, mAddInfo, dragOffset, dragRegion, scale, scale, options);
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index 5387be8..ebc2a25 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -20,10 +20,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetInfo;
 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 6944879..bef91d2 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -31,12 +31,10 @@
 import android.widget.TextView;
 
 import com.android.launcher3.BaseActivity;
+import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.SimpleOnStylusPressListener;
-import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.model.WidgetItem;
 
@@ -57,7 +55,7 @@
     private static final int FADE_IN_DURATION_MS = 90;
 
     /** Widget cell width is calculated by multiplying this factor to grid cell width. */
-    private static final float WIDTH_SCALE = 2.6f;
+    private static final float WIDTH_SCALE = 3f;
 
     /** Widget preview width is calculated by multiplying this factor to the widget cell width. */
     private static final float PREVIEW_SCALE = 0.8f;
@@ -72,7 +70,6 @@
     protected WidgetItem mItem;
 
     private WidgetPreviewLoader mWidgetPreviewLoader;
-    private StylusEventHelper mStylusEventHelper;
 
     protected CancellationSignal mActiveRequest;
     private boolean mAnimatePreview = true;
@@ -81,7 +78,8 @@
     private Bitmap mDeferredBitmap;
 
     protected final BaseActivity mActivity;
-    protected DeviceProfile mDeviceProfile;
+    protected final DeviceProfile mDeviceProfile;
+    private final CheckLongPressHelper mLongPressHelper;
 
     public WidgetCell(Context context) {
         this(context, null);
@@ -96,8 +94,9 @@
 
         mActivity = BaseActivity.fromContext(context);
         mDeviceProfile = mActivity.getDeviceProfile();
-        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
+        mLongPressHelper = new CheckLongPressHelper(this);
 
+        mLongPressHelper.setLongPressTimeoutFactor(1);
         setContainerWidth();
         setWillNotDraw(false);
         setClipToPadding(false);
@@ -105,7 +104,7 @@
     }
 
     private void setContainerWidth() {
-        mCellSize = (int) (mDeviceProfile.allAppsCellWidthPx * WIDTH_SCALE);
+        mCellSize = (int) (mDeviceProfile.allAppsIconSizePx * WIDTH_SCALE);
         mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
     }
 
@@ -182,10 +181,8 @@
             return;
         }
         if (bitmap != null) {
-            mWidgetImage.setBitmap(bitmap,
-                    DrawableFactory.INSTANCE.get(getContext()).getBadgeForUser(mItem.user,
-                            getContext(), BaseIconFactory.getBadgeSizeForIconSize(
-                                    mDeviceProfile.allAppsIconSizePx)));
+            mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
+                    BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
                 mWidgetImage.setAlpha(0f);
                 ViewPropertyAnimator anim = mWidgetImage.animate();
@@ -213,11 +210,15 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        boolean handled = super.onTouchEvent(ev);
-        if (mStylusEventHelper.onMotionEvent(ev)) {
-            return true;
-        }
-        return handled;
+        super.onTouchEvent(ev);
+        mLongPressHelper.onTouchEvent(ev);
+        return true;
+    }
+
+    @Override
+    public void cancelLongPress() {
+        super.cancelLongPress();
+        mLongPressHelper.cancelLongPress();
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index 8dcdd44..c022374 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -13,7 +13,6 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -103,7 +102,7 @@
                 if (LOGD) {
                     Log.d(TAG, "Binding widget, id: " + mWidgetLoadingId);
                 }
-                if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed(
+                if (new WidgetManagerHelper(mLauncher).bindAppWidgetIdIfAllowed(
                         mWidgetLoadingId, pInfo, options)) {
 
                     // Widget id bound. Inflate the widget.
diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/WidgetListRowEntry.java
index 335b8c7..17e4673 100644
--- a/src/com/android/launcher3/widget/WidgetListRowEntry.java
+++ b/src/com/android/launcher3/widget/WidgetListRowEntry.java
@@ -15,9 +15,9 @@
  */
 package com.android.launcher3.widget;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
 
 import java.util.ArrayList;
 
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
new file mode 100644
index 0000000..4b6c569
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2014 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.annotation.TargetApi;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.Utilities;
+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;
+
+/**
+ * Utility class to working with {@link AppWidgetManager}
+ */
+public class WidgetManagerHelper {
+
+    //TODO: replace this with OPTION_APPWIDGET_RESTORE_COMPLETED b/63667276
+    public static final String WIDGET_OPTION_RESTORE_COMPLETED = "appWidgetRestoreCompleted";
+
+    final AppWidgetManager mAppWidgetManager;
+    final Context mContext;
+
+    public WidgetManagerHelper(Context context) {
+        mContext = context;
+        mAppWidgetManager = AppWidgetManager.getInstance(context);
+    }
+
+    /**
+     * @see AppWidgetManager#getAppWidgetInfo(int)
+     */
+    public LauncherAppWidgetProviderInfo getLauncherAppWidgetInfo(int appWidgetId) {
+        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+            return CustomWidgetManager.INSTANCE.get(mContext).getWidgetProvider(appWidgetId);
+        }
+        AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
+        return info == null ? null : LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+    }
+
+    /**
+     * @see AppWidgetManager#getInstalledProvidersForPackage(String, UserHandle)
+     */
+    @TargetApi(Build.VERSION_CODES.O)
+    public List<AppWidgetProviderInfo> getAllProviders(@Nullable PackageUserKey packageUser) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return Collections.emptyList();
+        }
+
+        if (packageUser == null) {
+            return allWidgetsSteam(mContext).collect(Collectors.toList());
+        }
+
+        if (Utilities.ATLEAST_OREO) {
+            return mAppWidgetManager.getInstalledProvidersForPackage(
+                    packageUser.mPackageName, packageUser.mUser);
+        }
+
+        String pkg = packageUser.mPackageName;
+        return Stream.concat(
+                // Only get providers for the given package/user.
+                mAppWidgetManager.getInstalledProvidersForProfile(packageUser.mUser)
+                        .stream()
+                        .filter(w -> w.provider.equals(pkg)),
+                Process.myUserHandle().equals(packageUser.mUser)
+                        && mContext.getPackageName().equals(pkg)
+                        ? CustomWidgetManager.INSTANCE.get(mContext).stream()
+                        : Stream.empty())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * @see AppWidgetManager#bindAppWidgetIdIfAllowed(int, UserHandle, ComponentName, Bundle)
+     */
+    public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info,
+            Bundle options) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return false;
+        }
+        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+            return true;
+        }
+        return mAppWidgetManager.bindAppWidgetIdIfAllowed(
+                appWidgetId, info.getProfile(), info.provider, options);
+    }
+
+    public LauncherAppWidgetProviderInfo findProvider(ComponentName provider, UserHandle user) {
+        if (WidgetsModel.GO_DISABLE_WIDGETS) {
+            return null;
+        }
+        for (AppWidgetProviderInfo info :
+                getAllProviders(new PackageUserKey(provider.getPackageName(), user))) {
+            if (info.provider.equals(provider)) {
+                return LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns if a AppWidgetProvider has marked a widget restored
+     */
+    public boolean isAppWidgetRestored(int appWidgetId) {
+        return !WidgetsModel.GO_DISABLE_WIDGETS && mAppWidgetManager.getAppWidgetOptions(
+                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(
+                UserCache.INSTANCE.get(context)
+                        .getUserProfiles()
+                        .stream()
+                        .flatMap(u -> awm.getInstalledProvidersForProfile(u).stream()),
+                CustomWidgetManager.INSTANCE.get(context).stream());
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index a7078a2..30be7a6 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -16,8 +16,8 @@
 
 package com.android.launcher3.widget;
 
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
@@ -28,17 +28,16 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.animation.Interpolator;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.Insettable;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
-import com.android.launcher3.anim.Interpolators;
+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 java.util.List;
@@ -156,7 +155,7 @@
         setupNavBarColor();
         mOpenCloseAnimator.setValues(
                 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
         mOpenCloseAnimator.start();
     }
 
@@ -191,9 +190,9 @@
                 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
     }
 
-    @Nullable
     @Override
-    public Animator createHintCloseAnim(float distanceToMove) {
-        return ObjectAnimator.ofInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom));
+    public void addHintCloseAnim(
+            float distanceToMove, Interpolator interpolator, PendingAnimation target) {
+        target.setInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom), interpolator);
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index 435125b..df6e2c3 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -18,15 +18,15 @@
 
 import android.util.Log;
 
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
 
 import java.util.ArrayList;
 import java.util.Iterator;
 
-import androidx.recyclerview.widget.RecyclerView;
-
 /**
  * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
  * methods accordingly.
@@ -137,7 +137,7 @@
     }
 
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
-        return curInfo.iconBitmap.equals(newInfo.iconBitmap) &&
-                !mIconCache.isDefaultIcon(curInfo.iconBitmap, curInfo.user);
+        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
index 521f511..68a3ec5 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -15,10 +15,11 @@
  */
 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.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
@@ -28,8 +29,8 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Insettable;
@@ -37,6 +38,8 @@
 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;
 
@@ -84,6 +87,11 @@
         onWidgetsBound();
     }
 
+    @VisibleForTesting
+    public WidgetsRecyclerView getRecyclerView() {
+        return mRecyclerView;
+    }
+
     @Override
     protected Pair<View, String> getAccessibilityTarget() {
         return Pair.create(mRecyclerView, getContext().getString(
@@ -124,7 +132,7 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int widthUsed;
         if (mInsets.bottom > 0) {
-            widthUsed = 0;
+            widthUsed = mInsets.left + mInsets.right;
         } else {
             Rect padding = mLauncher.getDeviceProfile().workspacePadding;
             widthUsed = Math.max(padding.left + padding.right,
@@ -145,7 +153,7 @@
 
         // Content is laid out as center bottom aligned
         int contentWidth = mContent.getMeasuredWidth();
-        int contentLeft = (width - contentWidth) / 2;
+        int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
         mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
                 contentLeft + contentWidth, height);
 
@@ -174,6 +182,7 @@
                     .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) {
@@ -183,7 +192,6 @@
                 }
             });
             post(() -> {
-                mRecyclerView.setLayoutFrozen(true);
                 mOpenCloseAnimator.start();
                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
             });
@@ -239,12 +247,16 @@
         return mAdapter.getItemCount();
     }
 
-    @Nullable
     @Override
-    public Animator createHintCloseAnim(float distanceToMove) {
-        AnimatorSet anim = new AnimatorSet();
-        anim.play(ObjectAnimator.ofFloat(mRecyclerView, TRANSLATION_Y, -distanceToMove));
-        anim.play(ObjectAnimator.ofFloat(mRecyclerView, ALPHA, 0.5f));
-        return anim;
+    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/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index c15557b..82d4110 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -19,11 +19,14 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -168,5 +171,6 @@
     }
 
     @Override
-    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
+    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index f20c83d..0b66ec0 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -29,8 +29,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetInfo;
 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;
@@ -38,10 +38,10 @@
 import com.android.systemui.plugins.CustomWidgetPlugin;
 import com.android.systemui.plugins.PluginListener;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 
 /**
  * CustomWidgetManager handles custom widgets implemented as a plugin.
@@ -51,30 +51,33 @@
     public static final MainThreadInitializedObject<CustomWidgetManager> INSTANCE =
             new MainThreadInitializedObject<>(CustomWidgetManager::new);
 
+    private final Context mContext;
     /**
      * auto provider Id is an ever-increasing number that serves as the providerId whenever a new
      * custom widget has been connected.
      */
     private int mAutoProviderId = 0;
     private final SparseArray<CustomWidgetPlugin> mPlugins;
-    private final SparseArray<WeakReference<Context>> mContexts;
     private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
     private final SparseArray<ComponentName> mWidgetsIdMap;
     private Consumer<PackageUserKey> mWidgetRefreshCallback;
 
     private CustomWidgetManager(Context context) {
+        mContext = context;
         mPlugins = new SparseArray<>();
-        mContexts = new SparseArray<>();
         mCustomWidgets = new ArrayList<>();
         mWidgetsIdMap = new SparseArray<>();
         PluginManagerWrapper.INSTANCE.get(context)
                 .addPluginListener(this, CustomWidgetPlugin.class, true);
     }
 
+    public void onDestroy() {
+        PluginManagerWrapper.INSTANCE.get(mContext).removePluginListener(this);
+    }
+
     @Override
     public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
         mPlugins.put(mAutoProviderId, plugin);
-        mContexts.put(mAutoProviderId, new WeakReference<>(context));
         List<AppWidgetProviderInfo> providers = AppWidgetManager.getInstance(context)
                 .getInstalledProvidersForProfile(Process.myUserHandle());
         if (providers.isEmpty()) return;
@@ -94,7 +97,6 @@
         int providerId = findProviderId(plugin);
         if (providerId == -1) return;
         mPlugins.remove(providerId);
-        mContexts.remove(providerId);
         mCustomWidgets.remove(getWidgetProvider(providerId));
         mWidgetsIdMap.remove(providerId);
     }
@@ -112,17 +114,16 @@
     public void onViewCreated(LauncherAppWidgetHostView view) {
         CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo();
         CustomWidgetPlugin plugin = mPlugins.get(info.providerId);
-        WeakReference<Context> context = mContexts.get(info.providerId);
         if (plugin == null) return;
-        plugin.onViewCreated(context == null ? null : context.get(), view);
+        plugin.onViewCreated(view);
     }
 
     /**
-     * Returns the list of custom widgets.
+     * Returns the stream of custom widgets.
      */
     @NonNull
-    public List<CustomAppWidgetProviderInfo> getCustomWidgets() {
-        return mCustomWidgets;
+    public Stream<CustomAppWidgetProviderInfo> stream() {
+        return mCustomWidgets.stream();
     }
 
     /**
@@ -156,13 +157,13 @@
         info.provider = new ComponentName(
                 context.getPackageName(), CLS_CUSTOM_WIDGET_PREFIX + providerId);
 
-        info.label = plugin.getLabel(context);
-        info.resizeMode = plugin.getResizeMode(context);
+        info.label = plugin.getLabel();
+        info.resizeMode = plugin.getResizeMode();
 
-        info.spanX = plugin.getSpanX(context);
-        info.spanY = plugin.getSpanY(context);
-        info.minSpanX = plugin.getMinSpanX(context);
-        info.minSpanY = plugin.getMinSpanY(context);
+        info.spanX = plugin.getSpanX();
+        info.spanY = plugin.getSpanY();
+        info.minSpanX = plugin.getMinSpanX();
+        info.minSpanY = plugin.getMinSpanY();
         return info;
     }
 
diff --git a/src_build_config/BuildConfig.java b/src_build_config/BuildConfig.java
index 36d7f4b..49aadf6 100644
--- a/src_build_config/BuildConfig.java
+++ b/src_build_config/BuildConfig.java
@@ -18,4 +18,5 @@
 
 public final class BuildConfig {
   public static final String APPLICATION_ID = "com.android.launcher3";
+  public static final boolean DEBUG = false;
 }
diff --git a/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java b/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
new file mode 100644
index 0000000..c57f07d
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
@@ -0,0 +1,50 @@
+/*
+ * 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.view.ViewGroup;
+import android.widget.EditText;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this plugin interface to replace the all apps recycler view of the all apps drawer.
+ */
+@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 = 3;
+
+    /** Following are the order that these methods should be called. */
+    void setup(ViewGroup parent, Activity activity, float allAppsContainerHeight);
+
+    /**
+     * When drag starts, pass window inset related fields and the progress to indicate
+     * whether user is swiping down or swiping up
+     */
+    void onDragStart(float progress);
+
+    /** progress is between [0, 1] 1: down, 0: up */
+    void setProgress(float progress);
+
+    /** Called when container animation stops, so that plugin can perform cleanups */
+    void onAnimationEnd(float progress);
+
+    /** pass over the search box object */
+    void setEditText(EditText editText);
+}
diff --git a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
index 47aa94b..56ebcc5 100644
--- a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
@@ -17,7 +17,6 @@
 package com.android.systemui.plugins;
 
 import android.appwidget.AppWidgetHostView;
-import android.content.Context;
 
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
@@ -33,40 +32,40 @@
     /**
      * The label to display to the user in the AppWidget picker.
      */
-    String getLabel(Context context);
+    String getLabel();
 
     /**
      * The default width of the widget when added to a host, in dp. The widget will get
      * at least this width, and will often be given more, depending on the host.
      */
-    int getSpanX(Context context);
+    int getSpanX();
 
     /**
      * The default height of the widget when added to a host, in dp. The widget will get
      * at least this height, and will often be given more, depending on the host.
      */
-    int getSpanY(Context context);
+    int getSpanY();
 
     /**
      * Minimum width (in dp) which the widget can be resized to. This field has no effect if it
      * is greater than minWidth or if horizontal resizing isn't enabled.
      */
-    int getMinSpanX(Context context);
+    int getMinSpanX();
 
     /**
      * Minimum height (in dp) which the widget can be resized to. This field has no effect if it
      * is greater than minHeight or if vertical resizing isn't enabled.
      */
-    int getMinSpanY(Context context);
+    int getMinSpanY();
 
     /**
      * The rules by which a widget can be resized.
      */
-    int getResizeMode(Context context);
+    int getResizeMode();
 
     /**
      * Notify the plugin that container of the widget has been rendered, where the custom widget
      * can be attached to.
      */
-    void onViewCreated(Context context, AppWidgetHostView parent);
+    void onViewCreated(AppWidgetHostView parent);
 }
diff --git a/src_plugins/com/android/systemui/plugins/OWNERS b/src_plugins/com/android/systemui/plugins/OWNERS
new file mode 100644
index 0000000..0514999
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OWNERS
@@ -0,0 +1,4 @@
+# When changing interface for this plugin OR when increasing version code, please add Alex
+# Only add other owners if Alex is not available
+per-file AllAppsSearchPlugin.java, globs = set noparent
+per-file AllAppsSearchPlugin.java = alexmang@google.com, hyunyoungs@google.com, sunnygoyal@google.com, twickham@google.com
diff --git a/src_plugins/com/android/systemui/plugins/OverlayPlugin.java b/src_plugins/com/android/systemui/plugins/OverlayPlugin.java
new file mode 100644
index 0000000..1edb692
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OverlayPlugin.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.plugins;
+
+import android.app.Activity;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.shared.LauncherExterns;
+import com.android.systemui.plugins.shared.LauncherOverlayManager;
+
+/**
+ * Implement this interface to add a -1 content on the home screen.
+ */
+@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
+public interface OverlayPlugin extends Plugin {
+    String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERLAY";
+    int VERSION = 1;
+
+    LauncherOverlayManager createOverlayManager(Activity activity, LauncherExterns externs);
+
+}
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
new file mode 100644
index 0000000..a434d07
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.plugins;
+
+import android.view.MotionEvent;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this interface to receive a callback when the user swipes right
+ * to left on the gesture area. It won't fire if the user has quick switched to a previous app
+ * (swiped right) and the current app isn't yet the active one (i.e., if swiping left would take
+ * the user to a more recent app).
+ */
+@ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
+        version = com.android.systemui.plugins.OverscrollPlugin.VERSION)
+public interface OverscrollPlugin extends Plugin {
+
+    String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
+    int VERSION = 4;
+
+    String DEVICE_STATE_LOCKED = "Locked";
+    String DEVICE_STATE_LAUNCHER = "Launcher";
+    String DEVICE_STATE_APP = "App";
+    String DEVICE_STATE_UNKNOWN = "Unknown";
+
+    /**
+     * @return true if the plugin is active and will accept overscroll gestures
+     */
+    boolean isActive();
+
+    /**
+     * Called when a touch has been recognized as an overscroll gesture.
+     * @param horizontalDistancePx Horizontal distance from the last finger location to the finger
+     *                               location when it first touched the screen.
+     * @param verticalDistancePx Horizontal distance from the last finger location to the finger
+     *                             location when it first touched the screen.
+     * @param thresholdPx Minimum distance for gesture.
+     * @param flingDistanceThresholdPx Minimum distance for gesture by fling.
+     * @param flingVelocityThresholdPx Minimum velocity for gesture by fling.
+     * @param deviceState String representing the current device state
+     * @param underlyingActivity String representing the currently active Activity
+     */
+    void onTouchEvent(MotionEvent event,
+                      int horizontalDistancePx,
+                      int verticalDistancePx,
+                      int thresholdPx,
+                      int flingDistanceThresholdPx,
+                      int flingVelocityThresholdPx,
+                      String deviceState,
+                      String underlyingActivity);
+
+    /**
+     * @return `true` if overscroll gesture handling should override all other gestures.
+     */
+    boolean blockOtherGestures();
+
+    /**
+     * @return `true` if the overscroll gesture can pan the underlying app.
+     */
+    boolean allowsUnderlyingActivityOverscroll();
+}
diff --git a/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
index 0ebea3d..cd9f33d 100644
--- a/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
+++ b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
@@ -34,9 +34,9 @@
     /**
      * Sets up the recents overview extra card and fills in data.
      *
-     * @param context Plugin context
+     * @param context     Plugin context
      * @param frameLayout PlaceholderView
-     * @param activity Recents activity to hold extra view
+     * @param activity    Recents activity to hold extra view
      */
     void setupView(Context context, FrameLayout frameLayout, Activity activity);
 }
diff --git a/src_plugins/com/android/systemui/plugins/ResourceProvider.java b/src_plugins/com/android/systemui/plugins/ResourceProvider.java
new file mode 100644
index 0000000..d1767a0
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/ResourceProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Plugin to support customizing resource
+ */
+@ProvidesInterface(action = ResourceProvider.ACTION, version = ResourceProvider.VERSION)
+public interface ResourceProvider extends Plugin {
+    String ACTION = "com.android.launcher3.action.PLUGIN_DYNAMIC_RESOURCE";
+    int VERSION = 1;
+
+    /**
+     * @see android.content.res.Resources#getInteger(int)
+     */
+    int getInt(int resId);
+
+    /**
+     * @see android.content.res.Resources#getFraction(int, int, int)
+     */
+    float getFraction(int resId);
+
+    /**
+     * @see android.content.res.Resources#getDimension(int)
+     */
+    float getDimension(int resId);
+
+    /**
+     * @see android.content.res.Resources#getColor(int)
+     */
+    int getColor(int resId);
+
+    /**
+     * @see android.content.res.Resources#getFloat(int)
+     */
+    float getFloat(int resId);
+}
diff --git a/src/com/android/launcher3/LauncherExterns.java b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
similarity index 63%
rename from src/com/android/launcher3/LauncherExterns.java
rename to src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
index 272bbf6..13e4999 100644
--- a/src/com/android/launcher3/LauncherExterns.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
@@ -14,19 +14,36 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.systemui.plugins.shared;
 
 import android.content.SharedPreferences;
 
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+
 /**
  * This interface defines the set of methods that the Launcher activity exposes. Methods
  * here should be safe to call from classes outside of com.android.launcher3.*
  */
 public interface LauncherExterns {
 
-    boolean setLauncherCallbacks(LauncherCallbacks callbacks);
-
+    /**
+     * Returns the shared main preference
+     */
     SharedPreferences getSharedPrefs();
 
-    void setLauncherOverlay(Launcher.LauncherOverlay overlay);
+    /**
+     * Returns the device specific preference
+     */
+    SharedPreferences getDevicePrefs();
+
+    /**
+     * Sets the overlay on the target activity
+     */
+    void setLauncherOverlay(LauncherOverlay overlay);
+
+    /**
+     * Executes the command, next time the overlay is hidden
+     */
+    void runOnOverlayHidden(Runnable runnable);
+
 }
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
new file mode 100644
index 0000000..ac02ba4
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.plugins.shared;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface to control the overlay on Launcher
+ */
+public interface LauncherOverlayManager extends Application.ActivityLifecycleCallbacks {
+
+    default void onDeviceProvideChanged() { }
+
+    default void onAttachedToWindow() { }
+    default void onDetachedFromWindow() { }
+
+    default void dump(String prefix, PrintWriter w) { }
+
+    default void openOverlay() { }
+
+    default void hideOverlay(boolean animate) {
+        hideOverlay(animate ? 200 : 0);
+    }
+
+    default void hideOverlay(int duration) { }
+
+    default boolean startSearch(byte[] config, Bundle extras) {
+        return false;
+    }
+
+    @Override
+    default void onActivityCreated(Activity activity, Bundle bundle) { }
+
+    @Override
+    default void onActivityStarted(Activity activity) { }
+
+    @Override
+    default void onActivityResumed(Activity activity) { }
+
+    @Override
+    default void onActivityPaused(Activity activity) { }
+
+    @Override
+    default void onActivityStopped(Activity activity) { }
+
+    @Override
+    default void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
+
+    @Override
+    default void onActivityDestroyed(Activity activity) { }
+
+    interface LauncherOverlay {
+
+        /**
+         * Touch interaction leading to overscroll has begun
+         */
+        void onScrollInteractionBegin();
+
+        /**
+         * Touch interaction related to overscroll has ended
+         */
+        void onScrollInteractionEnd();
+
+        /**
+         * Scroll progress, between 0 and 100, when the user scrolls beyond the leftmost
+         * screen (or in the case of RTL, the rightmost screen).
+         */
+        void onScrollChange(float progress, boolean rtl);
+
+        /**
+         * Called when the launcher is ready to use the overlay
+         * @param callbacks A set of callbacks provided by Launcher in relation to the overlay
+         */
+        void setOverlayCallbacks(LauncherOverlayCallbacks callbacks);
+    }
+
+    interface LauncherOverlayCallbacks {
+
+        void onScrollChanged(float progress);
+    }
+}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 789bfd8..dcb4636 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,12 +16,14 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -31,8 +33,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 7a7f828..9d87788 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -3,30 +3,35 @@
 
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
 
+import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
+
 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;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AppFilter;
-import com.android.launcher3.icons.ComponentWithLabel;
-import com.android.launcher3.icons.IconCache;
 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.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.MultiHashMap;
 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.WidgetManagerHelper;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -37,8 +42,6 @@
 import java.util.Map.Entry;
 import java.util.Set;
 
-import androidx.annotation.Nullable;
-
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
  *
@@ -46,6 +49,9 @@
  */
 public class WidgetsModel {
 
+    // True is the widget support is disabled.
+    public static final boolean GO_DISABLE_WIDGETS = false;
+
     private static final String TAG = "WidgetsModel";
     private static final boolean DEBUG = false;
 
@@ -81,18 +87,19 @@
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
      */
-    public List<ComponentWithLabel> update(LauncherAppState app, @Nullable PackageUserKey packageUser) {
+    public List<ComponentWithLabelAndIcon> update(
+            LauncherAppState app, @Nullable PackageUserKey packageUser) {
         Preconditions.assertWorkerThread();
 
         Context context = app.getContext();
         final ArrayList<WidgetItem> widgetsAndShortcuts = new ArrayList<>();
-        List<ComponentWithLabel> updatedItems = new ArrayList<>();
+        List<ComponentWithLabelAndIcon> updatedItems = new ArrayList<>();
         try {
             InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
             PackageManager pm = app.getContext().getPackageManager();
 
             // Widgets
-            AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(context);
+            WidgetManagerHelper widgetManager = new WidgetManagerHelper(context);
             for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(packageUser)) {
                 LauncherAppWidgetProviderInfo launcherWidgetInfo =
                         LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
@@ -103,14 +110,14 @@
             }
 
             // Shortcuts
-            for (ShortcutConfigActivityInfo info : LauncherAppsCompat.getInstance(context)
-                    .getCustomShortcutActivityList(packageUser)) {
+            for (ShortcutConfigActivityInfo info :
+                    queryList(context, packageUser)) {
                 widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
                 updatedItems.add(info);
             }
             setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
         } catch (Exception e) {
-            if (!FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isBinderSizeError(e)) {
+            if (!FeatureFlags.IS_STUDIO_BUILD && Utilities.isBinderSizeError(e)) {
                 // the returned value may be incomplete and will not be refreshed until the next
                 // time Launcher starts.
                 // TODO: after figuring out a repro step, introduce a dirty bit to check when
@@ -238,4 +245,20 @@
             }
         }
     }
+
+    public WidgetItem getWidgetProviderInfoByProviderName(
+            ComponentName providerName) {
+        ArrayList<WidgetItem> widgetsList = mWidgetsList.get(
+                new PackageItemInfo(providerName.getPackageName()));
+        if (widgetsList == null) {
+            return null;
+        }
+
+        for (WidgetItem item : widgetsList) {
+            if (item.componentName.equals(providerName)) {
+                return item;
+            }
+        }
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 57f4164..0000000
--- a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shortcuts;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ShortcutQuery;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
-    private static final String TAG = "DeepShortcutManager";
-
-    private static final int FLAG_GET_ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
-            | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
-
-    private static DeepShortcutManager sInstance;
-
-    public static DeepShortcutManager getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new DeepShortcutManager(context.getApplicationContext());
-        }
-        return sInstance;
-    }
-
-    private final LauncherApps mLauncherApps;
-
-    private DeepShortcutManager(Context context) {
-        mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
-    }
-
-    /**
-     * Queries for the shortcuts with the package name and provided ids.
-     *
-     * This method is intended to get the full details for shortcuts when they are added or updated,
-     * because we only get "key" fields in onShortcutsChanged().
-     */
-    public QueryResult queryForFullDetails(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return query(FLAG_GET_ALL, packageName, null, shortcutIds, user);
-    }
-
-    /**
-     * Gets all the manifest and dynamic shortcuts associated with the given package and user,
-     * to be displayed in the shortcuts container on long press.
-     */
-    public QueryResult queryForShortcutsContainer(@Nullable ComponentName activity,
-            UserHandle user) {
-        if (activity == null) return QueryResult.FAILURE;
-        return query(ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_DYNAMIC,
-                activity.getPackageName(), activity, null, user);
-    }
-
-    /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void unpinShortcut(final ShortcutKey key) {
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
-        pinnedIds.remove(id);
-        try {
-            mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.w(TAG, "Failed to unpin shortcut", e);
-        }
-    }
-
-    /**
-     * Adds the given shortcut to the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void pinShortcut(final ShortcutKey key) {
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
-        pinnedIds.add(id);
-        try {
-            mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.w(TAG, "Failed to pin shortcut", e);
-        }
-    }
-
-    public void startShortcut(String packageName, String id, Rect sourceBounds,
-          Bundle startActivityOptions, UserHandle user) {
-        try {
-            mLauncherApps.startShortcut(packageName, id, sourceBounds,
-                    startActivityOptions, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to start shortcut", e);
-        }
-    }
-
-    public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
-        try {
-            return mLauncherApps.getShortcutIconDrawable(shortcutInfo, density);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to get shortcut icon", e);
-            return null;
-        }
-    }
-
-    /**
-     * Returns the id's of pinned shortcuts associated with the given package and user.
-     *
-     * If packageName is null, returns all pinned shortcuts regardless of package.
-     */
-    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
-        return queryForPinnedShortcuts(packageName, null, user);
-    }
-
-    public QueryResult queryForPinnedShortcuts(String packageName, List<String> shortcutIds,
-            UserHandle user) {
-        return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, shortcutIds, user);
-    }
-
-    public QueryResult queryForAllShortcuts(UserHandle user) {
-        return query(FLAG_GET_ALL, null, null, null, user);
-    }
-
-    private static List<String> extractIds(List<ShortcutInfo> shortcuts) {
-        List<String> shortcutIds = new ArrayList<>(shortcuts.size());
-        for (ShortcutInfo shortcut : shortcuts) {
-            shortcutIds.add(shortcut.getId());
-        }
-        return shortcutIds;
-    }
-
-    /**
-     * Query the system server for all the shortcuts matching the given parameters.
-     * If packageName == null, we query for all shortcuts with the passed flags, regardless of app.
-     *
-     * TODO: Use the cache to optimize this so we don't make an RPC every time.
-     */
-    private QueryResult query(int flags, String packageName, ComponentName activity,
-            List<String> shortcutIds, UserHandle user) {
-        ShortcutQuery q = new ShortcutQuery();
-        q.setQueryFlags(flags);
-        if (packageName != null) {
-            q.setPackage(packageName);
-            q.setActivity(activity);
-            q.setShortcutIds(shortcutIds);
-        }
-        try {
-            return new QueryResult(mLauncherApps.getShortcuts(q, user));
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to query for shortcuts", e);
-            return QueryResult.FAILURE;
-        }
-    }
-
-    public boolean hasHostPermission() {
-        try {
-            return mLauncherApps.hasShortcutHostPermission();
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to make shortcut manager call", e);
-        }
-        return false;
-    }
-
-    public static class QueryResult extends ArrayList<ShortcutInfo> {
-
-        static QueryResult FAILURE = new QueryResult();
-
-        private final boolean mWasSuccess;
-
-        QueryResult(List<ShortcutInfo> result) {
-            super(result == null ? Collections.emptyList() : result);
-            mWasSuccess = true;
-        }
-
-        QueryResult() {
-            mWasSuccess = false;
-        }
-
-
-        public boolean wasSuccess() {
-            return mWasSuccess;
-        }
-    }
-}
diff --git a/src_flags/com/android/launcher3/config/FeatureFlags.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
similarity index 62%
rename from src_flags/com/android/launcher3/config/FeatureFlags.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index 73c6996..6fd147a 100644
--- a/src_flags/com/android/launcher3/config/FeatureFlags.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,15 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.config;
+package com.android.launcher3.uioverrides;
 
-import android.content.Context;
+import android.app.Person;
+import android.content.pm.ShortcutInfo;
 
-/**
- * Defines a set of flags used to control various launcher behaviors
- */
-public final class FeatureFlags extends BaseFlags {
-    private FeatureFlags() {
-        // Prevent instantiation
+import com.android.launcher3.Utilities;
+
+public class ApiWrapper {
+
+    public static Person[] getPersons(ShortcutInfo si) {
+        return Utilities.EMPTY_PERSON_ARRAY;
     }
 }
diff --git a/src/com/android/launcher3/views/Transposable.java b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
similarity index 68%
rename from src/com/android/launcher3/views/Transposable.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
index 929c1aa..5c1ac28 100644
--- a/src/com/android/launcher3/views/Transposable.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,14 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.views;
 
-import com.android.launcher3.graphics.RotationMode;
+package com.android.launcher3.uioverrides;
 
-/**
- * Indicates that a view can be transposed.
- */
-public interface Transposable {
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
 
-    RotationMode getRotationMode();
+public class DeviceFlag extends DebugFlag {
+
+    public DeviceFlag(String key, boolean defaultValue, String description) {
+        super(key, defaultValue, description);
+    }
 }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/DisplayRotationListener.java b/src_ui_overrides/com/android/launcher3/uioverrides/DisplayRotationListener.java
deleted file mode 100644
index b1a67e9..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/DisplayRotationListener.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import android.content.Context;
-import android.view.OrientationEventListener;
-
-/**
- * Utility class for listening for rotation changes
- */
-public class DisplayRotationListener extends OrientationEventListener {
-
-    private final Runnable mCallback;
-
-    public DisplayRotationListener(Context context, Runnable callback) {
-        super(context);
-        mCallback = callback;
-    }
-
-    @Override
-    public void onOrientationChanged(int i) {
-        mCallback.run();
-    }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java b/src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
new file mode 100644
index 0000000..4893c17
--- /dev/null
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+/** A util class that inflates a predicted app icon */
+public class PredictedAppIconInflater {
+    public static View inflate(LayoutInflater inflater, ViewGroup parent, WorkspaceItemInfo info) {
+        return null;
+    }
+}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java b/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
deleted file mode 100644
index 60f12d8..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import android.content.Context;
-import com.android.launcher3.config.BaseFlags.BaseTogglableFlag;
-
-public class TogglableFlag extends BaseTogglableFlag {
-
-    public TogglableFlag(String key, boolean defaultValue, String description) {
-        super(key, defaultValue, description);
-    }
-
-    @Override
-    public boolean getOverridenDefaultValue(boolean value) {
-        return value;
-    }
-
-    @Override
-    public void addChangeListener(Context context, Runnable r) { }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
deleted file mode 100644
index f2b5ed2..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ /dev/null
@@ -1,106 +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.uioverrides;
-
-import android.app.Activity;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.content.pm.ShortcutInfo;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.util.TouchController;
-
-import java.io.PrintWriter;
-
-public class UiFactory {
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        return new TouchController[] {
-                launcher.getDragController(), new AllAppsSwipeController(launcher)};
-    }
-
-    public static Runnable enableLiveUIChanges(Launcher l) {
-        return null;
-    }
-
-    public static StateHandler[] getStateHandler(Launcher launcher) {
-        return new StateHandler[] {
-                launcher.getAllAppsController(), launcher.getWorkspace() };
-    }
-
-    public static void resetOverview(Launcher launcher) { }
-
-    public static void onLauncherStateOrFocusChanged(Launcher launcher) { }
-
-    public static void onCreate(Launcher launcher) { }
-
-    public static void onStart(Launcher launcher) { }
-
-    public static void onEnterAnimationComplete(Context context) {}
-
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) { }
-
-    public static void onTrimMemory(Launcher launcher, int level) { }
-
-    public static void useFadeOutAnimationForLauncherStart(Launcher launcher,
-            CancellationSignal cancellationSignal) { }
-
-    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
-        return false;
-    }
-
-    public static void setBackButtonAlpha(Launcher launcher, float alpha, boolean animate) { }
-
-
-    public static ScaleAndTranslation getOverviewScaleAndTranslationForNormalState(Launcher l) {
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
-    public static RotationMode getRotationMode(DeviceProfile dp) {
-        return RotationMode.NORMAL;
-    }
-
-    public static boolean startIntentSenderForResult(Activity activity, IntentSender intent,
-            int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
-            Bundle options) {
-        return false;
-    }
-
-    public static boolean startActivityForResult(Activity activity, Intent intent, int requestCode,
-            Bundle options) {
-        return false;
-    }
-
-    public static void resetPendingActivityResults(Launcher launcher, int requestCode) { }
-
-    public static void clearSwipeSharedState(boolean finishAnimation) {}
-
-    public static Person[] getPersons(ShortcutInfo si) {
-        return Utilities.EMPTY_PERSON_ARRAY;
-    }
-
-    public static void closeSystemWindows() {}
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java
index b05e125..b3aa365 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java
@@ -16,18 +16,19 @@
 
 package com.android.launcher3.uioverrides;
 
+import static android.app.WallpaperManager.FLAG_SYSTEM;
+
 import android.content.Context;
 import android.graphics.Color;
 import android.util.Pair;
 
+import com.android.launcher3.uioverrides.dynamicui.ColorExtractionAlgorithm;
 import com.android.launcher3.uioverrides.dynamicui.WallpaperColorsCompat;
 import com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompat;
-import com.android.launcher3.uioverrides.dynamicui.ColorExtractionAlgorithm;
+import com.android.launcher3.util.MainThreadInitializedObject;
 
 import java.util.ArrayList;
 
-import static android.app.WallpaperManager.FLAG_SYSTEM;
-
 public class WallpaperColorInfo implements WallpaperManagerCompat.OnColorsChangedListenerCompat {
 
     private static final int MAIN_COLOR_LIGHT = 0xffdadce0;
@@ -35,17 +36,9 @@
     private static final int MAIN_COLOR_REGULAR = 0xff000000;
 
     private static final int FALLBACK_COLOR = Color.WHITE;
-    private static final Object sInstanceLock = new Object();
-    private static WallpaperColorInfo sInstance;
 
-    public static WallpaperColorInfo getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                sInstance = new WallpaperColorInfo(context.getApplicationContext());
-            }
-            return sInstance;
-        }
-    }
+    public static final MainThreadInitializedObject<WallpaperColorInfo> INSTANCE =
+            new MainThreadInitializedObject<>(WallpaperColorInfo::new);
 
     private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
     private final WallpaperManagerCompat mWallpaperManager;
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
index 7006d77..ec3f93f 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -15,11 +15,10 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 
-import com.android.launcher3.AbstractFloatingView;
+import android.content.Context;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -32,7 +31,7 @@
 
     private static final float PARALLAX_COEFFICIENT = .125f;
 
-    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY;
+    private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE;
 
     private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) {
         @Override
@@ -42,17 +41,12 @@
     };
 
     public AllAppsState(int id) {
-        super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, STATE_FLAGS);
+        super(id, ContainerType.ALLAPPS, STATE_FLAGS);
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-        }
-
-        AbstractFloatingView.closeAllOpenViews(launcher);
-        dispatchWindowStateChanged(launcher);
+    public int getTransitionDuration(Context context) {
+        return 320;
     }
 
     @Override
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
index aeba788..7a6332c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+import android.content.Context;
 
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -26,7 +26,12 @@
 public class OverviewState extends LauncherState {
 
     public OverviewState(int id) {
-        super(id, ContainerType.WORKSPACE, OVERVIEW_TRANSITION_MS, FLAG_DISABLE_RESTORE);
+        super(id, ContainerType.WORKSPACE, FLAG_DISABLE_RESTORE);
+    }
+
+    @Override
+    public int getTransitionDuration(Context context) {
+        return 250;
     }
 
     public static OverviewState newBackgroundState(int id) {
@@ -40,4 +45,11 @@
     public static OverviewState newSwitchState(int id) {
         return new OverviewState(id);
     }
+
+    /**
+     *  New Overview substate that represents the overview in modal mode (one task shown on its own)
+     */
+    public static OverviewState newModalTaskState(int id) {
+        return new OverviewState(id);
+    }
 }
diff --git a/tests/Android.mk b/tests/Android.mk
index 02ead4e..4d1bfa6 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -36,7 +36,7 @@
 endif
 
 LOCAL_MODULE := ub-launcher-aosp-tapl
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := system_current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
@@ -50,18 +50,23 @@
     androidx.test.runner \
     androidx.test.rules \
     androidx.test.uiautomator_uiautomator \
-    mockito-target-minus-junit4
+    mockito-target-minus-junit4 \
+    launcher_log_protos_lite
 
 ifneq (,$(wildcard frameworks/base))
     LOCAL_PRIVATE_PLATFORM_APIS := true
     LOCAL_STATIC_JAVA_LIBRARIES += launcher-aosp-tapl
 else
-    LOCAL_SDK_VERSION := 28
+    LOCAL_SDK_VERSION := system_28
     LOCAL_MIN_SDK_VERSION := 21
     LOCAL_STATIC_JAVA_LIBRARIES += ub-launcher-aosp-tapl
 endif
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := \
+	$(call all-java-files-under, src) \
+	$(call all-java-files-under, src_common)
+
+
 LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_PACKAGE_NAME := Launcher3Tests
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index ffa90b9..1c8f095 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -22,6 +22,7 @@
 
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
@@ -79,8 +80,12 @@
             </intent-filter>
         </activity>
 
+        <service
+            android:name="com.android.launcher3.testcomponent.ListViewService"
+            android:permission="android.permission.BIND_REMOTEVIEWS" />
+
         <provider
-            android:name="com.android.launcher3.testcomponent.TestCommandReceiver"
+            android:name="com.android.launcher3.testcomponent.TestCommandProvider"
             android:authorities="${packageName}.commands"
             android:exported="true"/>
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 24b5b02..5cf96c8 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,7 +18,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3.tests">
 
-    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="25"
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"
               tools:overrideLibrary="android.support.test.uiautomator.v18"/>
 
     <application android:debuggable="true">
diff --git a/tests/dummy_app/AndroidManifest.xml b/tests/dummy_app/AndroidManifest.xml
index 9d0a74a..f00138c 100644
--- a/tests/dummy_app/AndroidManifest.xml
+++ b/tests/dummy_app/AndroidManifest.xml
@@ -21,7 +21,7 @@
      to come from a domain that you own or have control over. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.example.android.aardwolf">
-    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="21"/>
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="21"/>
     <application android:label="Aardwolf">
         <activity
             android:name="Activity1"
diff --git a/tests/res/layout/test_layout_widget_list.xml b/tests/res/layout/test_layout_widget_list.xml
new file mode 100644
index 0000000..0152040
--- /dev/null
+++ b/tests/res/layout/test_layout_widget_list.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#FFFFFF">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="#FF0000FF"
+        android:id="@android:id/text1"
+        android:padding="10dp" />
+
+    <ListView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:id="@android:id/list" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
index 293b04a..bdf01f3 100644
--- a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
+++ b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
@@ -20,14 +20,14 @@
 
 import android.content.ComponentName;
 
-import com.android.launcher3.AppInfo;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.model.data.AppInfo;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 /**
  * Unit tests for {@link DefaultAppSearchAlgorithm}
  */
diff --git a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
index efbd9c9..33066e4 100644
--- a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
@@ -49,7 +49,7 @@
         super.setUp();
         mDevice.pressHome();
         waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
         mSessionId = -1;
     }
 
diff --git a/tests/src/com/android/launcher3/testcomponent/ListViewService.java b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
new file mode 100644
index 0000000..3da20e0
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.testcomponent;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+public class ListViewService extends RemoteViewsService {
+
+    public static IBinder sBinderForTest;
+
+    @Override
+    public RemoteViewsFactory onGetViewFactory(Intent intent) {
+        return new SimpleViewsFactory();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sBinderForTest != null ? sBinderForTest : super.onBind(intent);
+    }
+
+    public static class SimpleViewsFactory implements RemoteViewsFactory {
+
+        public int viewCount = 0;
+
+        @Override
+        public void onCreate() { }
+
+        @Override
+        public void onDataSetChanged() { }
+
+        @Override
+        public void onDestroy() { }
+
+        @Override
+        public int getCount() {
+            return viewCount;
+        }
+
+        @Override
+        public RemoteViews getViewAt(int i) {
+            RemoteViews views = new RemoteViews("android", android.R.layout.simple_list_item_1);
+            views.setTextViewText(android.R.id.text1, getLabel(i));
+            return views;
+        }
+
+        public String getLabel(int i) {
+            return "Item " + i;
+        }
+
+        @Override
+        public RemoteViews getLoadingView() {
+            return null;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public long getItemId(int i) {
+            return i;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return false;
+        }
+
+        public IBinder toBinder() {
+            return new RemoteViewsService() {
+                @Override
+                public RemoteViewsFactory onGetViewFactory(Intent intent) {
+                    return SimpleViewsFactory.this;
+                }
+            }.onBind(new Intent("dummy_intent"));
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandProvider.java b/tests/src/com/android/launcher3/testcomponent/TestCommandProvider.java
new file mode 100644
index 0000000..f9981a9
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/TestCommandProvider.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.testcomponent;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+
+import static com.android.launcher3.testcomponent.TestCommandReceiver.DISABLE_TEST_LAUNCHER;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.ENABLE_TEST_LAUNCHER;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.KILL_PROCESS;
+import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.util.Base64;
+
+import com.android.launcher3.tapl.TestHelpers;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class TestCommandProvider extends ContentProvider {
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        switch (method) {
+            case ENABLE_TEST_LAUNCHER: {
+                getContext().getPackageManager().setComponentEnabledSetting(
+                        new ComponentName(getContext(), TestLauncherActivity.class),
+                        COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
+                return null;
+            }
+            case DISABLE_TEST_LAUNCHER: {
+                getContext().getPackageManager().setComponentEnabledSetting(
+                        new ComponentName(getContext(), TestLauncherActivity.class),
+                        COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
+                return null;
+            }
+            case KILL_PROCESS: {
+                ((ActivityManager) getContext().getSystemService(Activity.ACTIVITY_SERVICE))
+                        .killBackgroundProcesses(arg);
+                return null;
+            }
+
+            case GET_SYSTEM_HEALTH_MESSAGE: {
+                final Bundle response = new Bundle();
+                response.putString("result",
+                        TestHelpers.getSystemHealthMessage(getContext(), Long.parseLong(arg)));
+                return response;
+            }
+
+            case SET_LIST_VIEW_SERVICE_BINDER: {
+                ListViewService.sBinderForTest = extras.getBinder(EXTRA_VALUE);
+                return null;
+            }
+        }
+        return super.call(method, arg, extras);
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        String path = Base64.encodeToString(uri.getPath().getBytes(),
+                Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
+        File file = new File(getContext().getCacheDir(), path);
+        if (!file.exists()) {
+            // Create an empty file so that we can pass its descriptor
+            try {
+                file.createNewFile();
+            } catch (IOException e) {
+            }
+        }
+
+        return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
index 4246096..eb6c3ed 100644
--- a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
+++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
@@ -15,126 +15,36 @@
  */
 package com.android.launcher3.testcomponent;
 
-import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
-import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-import static android.content.pm.PackageManager.DONT_KILL_APP;
-import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
-
-import android.app.Activity;
-import android.app.ActivityManager;
 import android.app.Instrumentation;
-import android.content.ComponentName;
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.util.Base64;
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.launcher3.tapl.TestHelpers;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
 /**
  * Content provider to receive commands from tests
  */
-public class TestCommandReceiver extends ContentProvider {
+public class TestCommandReceiver {
 
     public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher";
     public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher";
     public static final String KILL_PROCESS = "kill-process";
     public static final String GET_SYSTEM_HEALTH_MESSAGE = "get-system-health-message";
+    public static final String SET_LIST_VIEW_SERVICE_BINDER = "set-list-view-service-binder";
 
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException("unimplemented mock method");
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        throw new UnsupportedOperationException("unimplemented mock method");
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        throw new UnsupportedOperationException("unimplemented mock method");
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        throw new UnsupportedOperationException("unimplemented mock method");
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException("unimplemented mock method");
-    }
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras) {
-        switch (method) {
-            case ENABLE_TEST_LAUNCHER: {
-                getContext().getPackageManager().setComponentEnabledSetting(
-                        new ComponentName(getContext(), TestLauncherActivity.class),
-                        COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
-                return null;
-            }
-            case DISABLE_TEST_LAUNCHER: {
-                getContext().getPackageManager().setComponentEnabledSetting(
-                        new ComponentName(getContext(), TestLauncherActivity.class),
-                        COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
-                return null;
-            }
-            case KILL_PROCESS: {
-                ((ActivityManager) getContext().getSystemService(Activity.ACTIVITY_SERVICE)).
-                        killBackgroundProcesses(arg);
-                return null;
-            }
-
-            case GET_SYSTEM_HEALTH_MESSAGE: {
-                final Bundle response = new Bundle();
-                response.putString("result",
-                        TestHelpers.getSystemHealthMessage(getContext(), Long.parseLong(arg)));
-                return response;
-            }
-        }
-        return super.call(method, arg, extras);
-    }
+    public static final String EXTRA_VALUE = "value";
 
     public static Bundle callCommand(String command) {
         return callCommand(command, null);
     }
 
     public static Bundle callCommand(String command, String arg) {
-        Instrumentation inst = InstrumentationRegistry.getInstrumentation();
-        Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands");
-        return inst.getTargetContext().getContentResolver().call(uri, command, arg, null);
+        return callCommand(command, arg, null);
     }
 
-    @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        String path = Base64.encodeToString(uri.getPath().getBytes(),
-                Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
-        File file = new File(getContext().getCacheDir(), path);
-        if (!file.exists()) {
-            // Create an empty file so that we can pass its descriptor
-            try {
-                file.createNewFile();
-            } catch (IOException e) {
-            }
-        }
-
-        return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
+    public static Bundle callCommand(String command, String arg, Bundle extras) {
+        Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands");
+        return inst.getTargetContext().getContentResolver().call(uri, command, arg, extras);
     }
 }
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 5174e4d..472e1a1 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -21,14 +21,17 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
-import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
 import androidx.test.InstrumentationRegistry;
@@ -86,7 +89,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -96,7 +99,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -104,7 +107,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener, never()).onDragStart(anyBoolean());
+        verify(mMockListener, never()).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -115,7 +118,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -126,7 +129,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -137,7 +140,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -148,7 +151,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -156,7 +159,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDrag(anyFloat(), anyObject());
+        verify(mMockListener).onDrag(anyFloat(), anyFloat(), any(MotionEvent.class));
     }
 
     @Test
@@ -168,4 +171,21 @@
         // TODO: actually calculate the following parameters and do exact value checks.
         verify(mMockListener).onDragEnd(anyFloat());
     }
+
+    @Test
+    public void testInterleavedSetState() {
+        doAnswer(invocationOnMock -> {
+            // Sets state to IDLE. (Normally onDragEnd() will have state SETTLING.)
+            mDetector.finishedScrolling();
+            return null;
+        }).when(mMockListener).onDragEnd(anyFloat());
+
+        mGenerator.put(0, 100, 100);
+        mGenerator.move(0, 100, 100 + mTouchSlop);
+        mGenerator.move(0, 100, 100 + mTouchSlop * 2);
+        mGenerator.lift(0);
+        verify(mMockListener).onDragEnd(anyFloat());
+        assertTrue("SwipeDetector should be IDLE but was " + mDetector.mState,
+                mDetector.isIdleState());
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 9a894b1..858e183 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,25 +15,30 @@
  */
 package com.android.launcher3.ui;
 
-import static com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import static java.lang.System.exit;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.os.Debug;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -44,26 +49,29 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.common.WidgetUtils;
 import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.FailureRewriterRule;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.LauncherActivityRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
@@ -79,6 +87,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Supplier;
 
 /**
  * Base class for all instrumentation tests providing various utility methods.
@@ -88,16 +97,75 @@
     public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
 
-    public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
+    public static final long DEFAULT_UI_TIMEOUT = 10000;
     private static final String TAG = "AbstractLauncherUiTest";
 
+    private static String sStrictmodeDetectedActivityLeak;
+    private static boolean sActivityLeakReported;
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    protected static final ActivityLeakTracker ACTIVITY_LEAK_TRACKER = new ActivityLeakTracker();
+
     protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
     protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
     protected final LauncherInstrumentation mLauncher = new LauncherInstrumentation();
     protected Context mTargetContext;
     protected String mTargetPackage;
+    private int mLauncherPid;
+
+    static {
+        if (TestHelpers.isInLauncherProcess()) {
+            StrictMode.VmPolicy.Builder builder =
+                    new StrictMode.VmPolicy.Builder()
+// b/154772063
+//                            .detectActivityLeaks()
+                            .penaltyLog()
+                            .penaltyListener(Runnable::run, violation -> {
+                                if (sStrictmodeDetectedActivityLeak == null) {
+                                    sStrictmodeDetectedActivityLeak = violation.toString() + ", "
+                                            + dumpHprofData() + ".";
+                                }
+                            });
+            StrictMode.setVmPolicy(builder.build());
+        }
+    }
+
+    public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
+        if (sActivityLeakReported) return;
+
+        if (sStrictmodeDetectedActivityLeak != null) {
+            // Report from the test thread strictmode violations detected in the main thread.
+            sActivityLeakReported = true;
+            Assert.fail(sStrictmodeDetectedActivityLeak);
+        }
+
+        // Check whether activity leak detector has found leaked activities.
+        Wait.atMost(AbstractLauncherUiTest::getActivityLeakErrorMessage,
+                () -> {
+                    launcher.getTotalPssKb();  // Triggers GC
+                    return MAIN_EXECUTOR.submit(
+                            () -> ACTIVITY_LEAK_TRACKER.noLeakedActivities()).get();
+                }, DEFAULT_UI_TIMEOUT, launcher);
+    }
+
+    private static String getActivityLeakErrorMessage() {
+        sActivityLeakReported = true;
+        return "Activity leak detector has found leaked activities, " + dumpHprofData() + ".";
+    }
+
+    private static String dumpHprofData() {
+        try {
+            final String fileName = getInstrumentation().getTargetContext().getFilesDir().getPath()
+                    + "/ActivityLeakHeapDump.hprof";
+            Debug.dumpHprofData(fileName);
+            return "memory dump filename: " + fileName;
+        } catch (Throwable e) {
+            Log.e(TAG, "dumpHprofData failed", e);
+            return "failed to save memory dump";
+        }
+    }
 
     protected AbstractLauncherUiTest() {
+        mLauncher.enableCheckEventsForSuccessfulGestures();
         try {
             mDevice.setOrientationNatural();
         } catch (RemoteException e) {
@@ -114,15 +182,13 @@
                                     checkLauncherIntegrity(launcher, containerType)));
         }
         mLauncher.enableDebugTracing();
+        // Avoid double-reporting of Launcher crashes.
+        mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
     }
 
     protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
 
     @Rule
-    public ShellCommandRule mDefaultLauncherRule =
-            TestHelpers.isInLauncherProcess() ? ShellCommandRule.setDefaultLauncher() : null;
-
-    @Rule
     public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
 
@@ -150,53 +216,93 @@
     }
 
     protected TestRule getRulesInsideActivityMonitor() {
-        return RuleChain.
-                outerRule(new PortraitLandscapeRunner(this)).
-                around(new FailureWatcher(mDevice));
+        final RuleChain inner = RuleChain.outerRule(new PortraitLandscapeRunner(this))
+                .around(new FailureWatcher(mDevice));
+
+        return TestHelpers.isInLauncherProcess()
+                ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher())
+                .around(inner) :
+                inner;
     }
 
     @Rule
     public TestRule mOrderSensitiveRules = RuleChain.
-            outerRule(new TestStabilityRule()).
-            around(mActivityMonitor).
-            around(getRulesInsideActivityMonitor());
+            outerRule(new FailureRewriterRule())
+            .around(new TestStabilityRule())
+            .around(mActivityMonitor)
+            .around(getRulesInsideActivityMonitor());
 
     public UiDevice getDevice() {
         return mDevice;
     }
 
+    private boolean hasSystemUiObject(String resId) {
+        return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
+    }
+
     @Before
     public void setUp() throws Exception {
+        Log.d(TAG, "Before disabling battery defender");
+        mDevice.executeShellCommand("setprop vendor.battery.defender.disable 1");
+        Log.d(TAG, "Before enabling stay awake");
+        mDevice.executeShellCommand("settings put global stay_on_while_plugged_in 3");
+        for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
+            Log.d(TAG, "Before unlocking the phone");
+            mDevice.executeShellCommand("input keyevent 82");
+            mDevice.waitForIdle();
+        }
+        Assert.assertTrue("Keyguard still visible",
+                mDevice.wait(
+                        Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
+        Log.d(TAG, "Keyguard is not visible");
+
+        final String launcherPackageName = mDevice.getLauncherPackageName();
+        try {
+            final Context context = InstrumentationRegistry.getContext();
+            final PackageManager pm = context.getPackageManager();
+            final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
+
+            if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
+                Assert.assertEquals("Launcher version doesn't match tests version",
+                        pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
+                        launcherPackage.getLongVersionCode());
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+
+        mLauncherPid = 0;
         // Disable app tracker
         AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
 
         mTargetContext = InstrumentationRegistry.getTargetContext();
         mTargetPackage = mTargetContext.getPackageName();
+        mLauncherPid = mLauncher.getPid();
+
+        UserManager userManager = mTargetContext.getSystemService(UserManager.class);
+        if (userManager != null) {
+            for (UserHandle userHandle : userManager.getUserProfiles()) {
+                if (!userHandle.isSystem()) {
+                    mDevice.executeShellCommand("pm remove-user " + userHandle.getIdentifier());
+                }
+            }
+        }
     }
 
     @After
     public void verifyLauncherState() {
-        try {
-            // Limits UI tests affecting tests running after them.
-            waitForModelLoaded();
-        } catch (Throwable t) {
-            Log.e(TAG,
-                    "Couldn't deinit after a test, exiting tests, see logs for failures that "
-                            + "could have caused this",
-                    t);
-            exit(1);
+        // Limits UI tests affecting tests running after them.
+        mLauncher.waitForLauncherInitialized();
+        if (mLauncherPid != 0) {
+            assertEquals("Launcher crashed, pid mismatch:",
+                    mLauncherPid, mLauncher.getPid().intValue());
         }
+        checkDetectedLeaks(mLauncher);
     }
 
-    protected void clearLauncherData() throws IOException, InterruptedException {
-        if (TestHelpers.isInLauncherProcess()) {
-            LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-            resetLoaderState();
-        } else {
-            clearPackageData(mDevice.getLauncherPackageName());
-            mLauncher.enableDebugTracing();
-        }
+    protected void clearLauncherData() {
+        mLauncher.clearLauncherData();
+        mLauncher.waitForLauncherInitialized();
     }
 
     /**
@@ -213,18 +319,24 @@
     protected void resetLoaderState() {
         try {
             mMainThreadExecutor.execute(
-                    () -> LauncherAppState.getInstance(mTargetContext).getModel().forceReload());
+                    () -> LauncherAppState.getInstance(
+                            mTargetContext).getModel().forceReload());
         } catch (Throwable t) {
             throw new IllegalArgumentException(t);
         }
-        waitForModelLoaded();
+        mLauncher.waitForLauncherInitialized();
     }
 
-    protected void waitForModelLoaded() {
-        waitForLauncherCondition("Launcher model didn't load", launcher -> {
-            final LauncherModel model = LauncherAppState.getInstance(mTargetContext).getModel();
-            return model.getCallback() == null || model.isModelLoaded();
-        });
+    /**
+     * Adds {@param item} on the homescreen on the 0th screen
+     */
+    protected void addItemToScreen(ItemInfo item) {
+        WidgetUtils.addItemToScreen(item, mTargetContext);
+        resetLoaderState();
+
+        // Launch the home activity
+        mDevice.pressHome();
+        mLauncher.waitForLauncherInitialized();
     }
 
     /**
@@ -233,7 +345,7 @@
     protected <T> T getOnUiThread(final Callable<T> callback) {
         try {
             return mMainThreadExecutor.submit(callback).get();
-        } catch (Exception e) {
+        } catch (Throwable e) {
             throw new RuntimeException(e);
         }
     }
@@ -250,11 +362,12 @@
         });
     }
 
-    // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting
+    // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call
+    // expecting
     // the results of that gesture because the wait can hide flakeness.
-    protected void waitForState(String message, LauncherState state) {
+    protected void waitForState(String message, Supplier<LauncherState> state) {
         waitForLauncherCondition(message,
-                launcher -> launcher.getStateManager().getCurrentStableState() == state);
+                launcher -> launcher.getStateManager().getCurrentStableState() == state.get());
     }
 
     protected void waitForResumed(String message) {
@@ -263,7 +376,8 @@
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
     // flakiness.
-    protected void waitForLauncherCondition(String message, Function<Launcher, Boolean> condition) {
+    protected void waitForLauncherCondition(String
+            message, Function<Launcher, Boolean> condition) {
         waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
     }
 
@@ -278,7 +392,7 @@
     protected void waitForLauncherCondition(
             String message, Function<Launcher, Boolean> condition, long timeout) {
         if (!TestHelpers.isInLauncherProcess()) return;
-        Wait.atMost(message, () -> getFromLauncher(condition), timeout);
+        Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
@@ -291,7 +405,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout);
+        }, timeout, mLauncher);
         return (T) output[0];
     }
 
@@ -305,13 +419,12 @@
         Wait.atMost(message, () -> {
             testThreadAction.run();
             return getFromLauncher(condition);
-        }, timeout);
+        }, timeout, mLauncher);
     }
 
     protected LauncherActivityInfo getSettingsApp() {
-        return LauncherAppsCompat.getInstance(mTargetContext)
-                .getActivityList("com.android.settings",
-                        Process.myUserHandle()).get(0);
+        return mTargetContext.getSystemService(LauncherApps.class)
+                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
     }
 
     /**
@@ -340,11 +453,12 @@
 
         public Intent blockingGetExtraIntent() throws InterruptedException {
             Intent intent = blockingGetIntent();
-            return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
+            return intent == null ? null : (Intent) intent.getParcelableExtra(
+                    Intent.EXTRA_INTENT);
         }
     }
 
-    protected void startAppFast(String packageName) {
+    public static void startAppFast(String packageName) {
         startIntent(
                 getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                         packageName),
@@ -352,7 +466,7 @@
                 true /* newTask */);
     }
 
-    protected void startTestActivity(int activityNumber) {
+    public static void startTestActivity(int activityNumber) {
         final String packageName = getAppPackageName();
         final Intent intent = getInstrumentation().getContext().getPackageManager().
                 getLaunchIntentForPackage(packageName);
@@ -362,23 +476,30 @@
                 false /* newTask */);
     }
 
-    private void startIntent(Intent intent, BySelector selector, boolean newTask) {
+    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
         if (newTask) {
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         } else {
-            intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+            intent.addFlags(
+                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
         }
         getInstrumentation().getTargetContext().startActivity(intent);
         assertTrue("App didn't start: " + selector,
-                mDevice.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
+                UiDevice.getInstance(getInstrumentation())
+                        .wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
     }
 
-    public static String resolveSystemApp(String category) {
+    public static ActivityInfo resolveSystemAppInfo(String category) {
         return getInstrumentation().getContext().getPackageManager().resolveActivity(
                 new Intent(Intent.ACTION_MAIN).addCategory(category),
                 PackageManager.MATCH_SYSTEM_ONLY).
-                activityInfo.packageName;
+                activityInfo;
+    }
+
+
+    public static String resolveSystemApp(String category) {
+        return resolveSystemAppInfo(category).packageName;
     }
 
     protected void closeLauncherActivity() {
@@ -396,9 +517,10 @@
         return !launcher.hasBeenResumed();
     }
 
-    protected boolean isInState(LauncherState state) {
+    protected boolean isInState(Supplier<LauncherState> state) {
         if (!TestHelpers.isInLauncherProcess()) return true;
-        return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
+        return getFromLauncher(
+                launcher -> launcher.getStateManager().getState() == state.get());
     }
 
     protected int getAllAppsScroll(Launcher launcher) {
@@ -408,7 +530,7 @@
     private static void checkLauncherIntegrity(
             Launcher launcher, ContainerType expectedContainerType) {
         if (launcher != null) {
-            final LauncherStateManager stateManager = launcher.getStateManager();
+            final StateManager<LauncherState> stateManager = launcher.getStateManager();
             final LauncherState stableState = stateManager.getCurrentStableState();
 
             assertTrue("Stable state != state: " + stableState.getClass().getSimpleName() + ", "
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
new file mode 100644
index 0000000..202dcb1
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ui;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.launcher3.tapl.TestHelpers;
+
+import java.util.WeakHashMap;
+
+public class ActivityLeakTracker implements Application.ActivityLifecycleCallbacks {
+    private final WeakHashMap<Activity, Boolean> mActivities = new WeakHashMap<>();
+
+    private int mActivitiesCreated;
+
+    ActivityLeakTracker() {
+        if (!TestHelpers.isInLauncherProcess()) return;
+        final Application app =
+                (Application) InstrumentationRegistry.getTargetContext().getApplicationContext();
+        app.registerActivityLifecycleCallbacks(this);
+    }
+
+    public int getActivitiesCreated() {
+        return mActivitiesCreated;
+    }
+
+    @Override
+    public void onActivityCreated(Activity activity, Bundle bundle) {
+        mActivities.put(activity, true);
+        ++mActivitiesCreated;
+    }
+
+    @Override
+    public void onActivityStarted(Activity activity) {
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+    }
+
+    @Override
+    public void onActivityPaused(Activity activity) {
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+    }
+
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
+    }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+    }
+
+    public boolean noLeakedActivities() {
+        int liveActivities = 0;
+        int destroyedActivities = 0;
+
+        for (Activity activity : mActivities.keySet()) {
+            if (activity.isDestroyed()) {
+                ++destroyedActivities;
+            } else {
+                ++liveActivities;
+            }
+        }
+
+        if (liveActivities > 2) return false;
+
+        // It's OK to have 1 leaked activity if no active activities exist.
+        return liveActivities == 0 ? destroyedActivities <= 1 : destroyedActivities == 0;
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
deleted file mode 100644
index ff21880..0000000
--- a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.ui;
-
-import static org.junit.Assert.assertTrue;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.net.Uri;
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.testcomponent.TestCommandReceiver;
-import com.android.launcher3.util.LauncherLayoutBuilder;
-import com.android.launcher3.util.rule.ShellCommandRule;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.OutputStreamWriter;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class DefaultLayoutProviderTest extends AbstractLauncherUiTest {
-
-    @Rule
-    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
-    private static final String SETTINGS_APP = "com.android.settings";
-
-    private Context mContext;
-    private String mAuthority;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        mContext = InstrumentationRegistry.getContext();
-
-        PackageManager pm = mTargetContext.getPackageManager();
-        ProviderInfo pi = pm.getProviderInfo(new ComponentName(mContext,
-                TestCommandReceiver.class), 0);
-        mAuthority = pi.authority;
-    }
-
-    @Test
-    public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
-        writeLayout(new LauncherLayoutBuilder().atHotseat(0).putApp(SETTINGS_APP, SETTINGS_APP));
-
-        // Launch the home activity
-        mDevice.pressHome();
-
-        mLauncher.getWorkspace().getHotseatAppIcon(getSettingsApp().getLabel().toString());
-    }
-
-    @Test
-    public void testCustomProfileLoaded_with_widget() throws Exception {
-        // A non-restored widget with no config screen gets restored automatically.
-        LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-
-        writeLayout(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
-                .putWidget(info.getComponent().getPackageName(),
-                        info.getComponent().getClassName(), 2, 2));
-
-        // Launch the home activity
-        mDevice.pressHome();
-
-        // Verify widget present
-        assertTrue("Widget is not present",
-                mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
-    }
-
-    @Test
-    public void testCustomProfileLoaded_with_folder() throws Exception {
-        writeLayout(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .build());
-
-        // Launch the home activity
-        mDevice.pressHome();
-
-        mLauncher.getWorkspace().getHotseatFolder("Folder: Copy");
-    }
-
-    @After
-    public void cleanup() throws Exception {
-        mDevice.executeShellCommand("settings delete secure launcher3.layout.provider");
-    }
-
-    private void writeLayout(LauncherLayoutBuilder builder) throws Exception {
-        mDevice.executeShellCommand("settings put secure launcher3.layout.provider " + mAuthority);
-        ParcelFileDescriptor pfd = mTargetContext.getContentResolver().openFileDescriptor(
-                Uri.parse("content://" + mAuthority + "/launcher_layout"), "w");
-
-        try (OutputStreamWriter writer = new OutputStreamWriter(new AutoCloseOutputStream(pfd))) {
-            builder.build(writer);
-        }
-        clearLauncherData();
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 80bb3ed..266f0ae 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -38,8 +38,8 @@
 
                     evaluateInPortrait();
                     evaluateInLandscape();
-                } catch (Exception e) {
-                    Log.e(TAG, "Exception", e);
+                } catch (Throwable e) {
+                    Log.e(TAG, "Error", e);
                     throw e;
                 } finally {
                     mTest.mDevice.setOrientationNatural();
@@ -56,6 +56,7 @@
             private void evaluateInPortrait() throws Throwable {
                 mTest.mDevice.setOrientationNatural();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
+                AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher);
                 base.evaluate();
                 mTest.getDevice().pressHome();
             }
@@ -63,6 +64,7 @@
             private void evaluateInLandscape() throws Throwable {
                 mTest.mDevice.setOrientationLeft();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
+                AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher);
                 base.evaluate();
                 mTest.getDevice().pressHome();
             }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 2cf6c2b..34e425d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -24,8 +24,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import android.util.Log;
-
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -38,13 +36,11 @@
 import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.launcher3.widget.WidgetsRecyclerView;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -63,10 +59,12 @@
         test.clearLauncherData();
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        test.waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        test.waitForState("Launcher internal state didn't switch to Home",
+                () -> LauncherState.NORMAL);
         test.waitForResumed("Launcher internal state is still Background");
         // Check that we switched to home.
         test.mLauncher.getWorkspace();
+        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher);
     }
 
     // Please don't add negative test cases for methods that fail only after a long wait.
@@ -104,7 +102,14 @@
     }
 
     @Test
-    @Ignore
+    public void testOpenHomeSettingsFromWorkspace() {
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        mLauncher.getOptionsPopupMenu().getMenuItem("Home settings")
+                .launch(mDevice.getLauncherPackageName());
+    }
+
+    @Test
     public void testPressHomeOnAllAppsContextMenu() throws Exception {
         final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
         allApps.freeze();
@@ -123,7 +128,7 @@
 
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
 
             // Test flinging forward and backward.
             test.executeOnLauncher(launcher -> assertEquals(
@@ -132,7 +137,7 @@
 
             allApps.flingForward();
             assertTrue("Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
             final Integer flingForwardY = test.getFromLauncher(
                     launcher -> test.getAllAppsScroll(launcher));
             test.executeOnLauncher(
@@ -142,7 +147,7 @@
             allApps.flingBackward();
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
             final Integer flingBackwardY = test.getFromLauncher(
                     launcher -> test.getAllAppsScroll(launcher));
             test.executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
@@ -159,7 +164,7 @@
 
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
         } finally {
             allApps.unfreeze();
         }
@@ -170,7 +175,8 @@
     public void testWorkspaceSwitchToAllApps() {
         assertNotNull("switchToAllApps() returned null",
                 mLauncher.getWorkspace().switchToAllApps());
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
     }
 
     @Test
@@ -196,7 +202,7 @@
 
         // Test flinging workspace.
         workspace.flingBackward();
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
         executeOnLauncher(
                 launcher -> assertEquals("Flinging back didn't switch workspace to page #0",
                         0, getCurrentWorkspacePage(launcher)));
@@ -205,7 +211,7 @@
         executeOnLauncher(
                 launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
                         1, getCurrentWorkspacePage(launcher)));
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
 
         // Test starting a workspace app.
         final AppIcon app = workspace.getWorkspaceAppIcon("Chrome");
@@ -231,7 +237,8 @@
     @PortraitLandscape
     public void testAppIconLaunchFromAllAppsFromHome() throws Exception {
         final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
 
         runIconLaunchFromAllAppsTest(this, allApps);
     }
@@ -307,7 +314,7 @@
                 switchToAllApps();
         allApps.freeze();
         try {
-            allApps.getAppIcon(APP_NAME).dragToWorkspace();
+            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
             mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
@@ -335,7 +342,7 @@
                     getMenuItem(0);
             final String shortcutName = menuItem.getText();
 
-            menuItem.dragToWorkspace();
+            menuItem.dragToWorkspace(false, false);
             mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
@@ -345,10 +352,4 @@
     public static String getAppPackageName() {
         return getInstrumentation().getContext().getPackageName();
     }
-
-    @Test
-    @Stability
-    public void testTestStabilityAttribute() {
-        Log.d("TestStabilityRule", "Hello world!");
-    }
 }
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index d0df664..eceff34 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -24,12 +24,10 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import androidx.test.uiautomator.UiDevice;
-
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.concurrent.Callable;
 import java.util.function.Function;
@@ -53,7 +51,7 @@
                                 hasConfigureScreen ? AppWidgetWithConfig.class
                                         : AppWidgetNoConfig.class);
                         Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
-                        return AppWidgetManagerCompat.getInstance(getTargetContext())
+                        return new WidgetManagerHelper(getTargetContext())
                                 .findProvider(cn, Process.myUserHandle());
                     }
                 });
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index d9edc35..8d571ff 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -16,23 +16,42 @@
 package com.android.launcher3.ui;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.widget.TextView;
+
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.WorkModeSwitch;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.WorkEduView;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+import java.util.Objects;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class WorkTabTest extends AbstractLauncherUiTest {
 
     private int mProfileUserId;
 
+    private static final int WORK_PAGE = AllAppsContainerView.AdapterHolder.WORK;
+
     @Before
     public void createWorkProfile() throws Exception {
         String output =
@@ -54,14 +73,119 @@
     @Test
     public void workTabExists() {
         mDevice.pressHome();
-        waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
+        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        waitForLauncherCondition("Personal tab is missing",
+                launcher -> launcher.getAppsView().isPersonalTabVisible(), 60000);
+        waitForLauncherCondition("Work tab is missing",
+                launcher -> launcher.getAppsView().isWorkTabVisible(), 60000);
+    }
+
+    @Test
+    public void toggleWorks() {
+        mDevice.pressHome();
+        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
+        getOnceNotNull("Apps view did not bind",
+                launcher -> launcher.getAppsView().getWorkModeSwitch(), 60000);
+
+        UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
+        assertEquals(2, userManager.getUserProfiles().size());
+        UserHandle workProfile = getFromLauncher(l -> {
+            UserHandle myHandle = Process.myUserHandle();
+            List<UserHandle> userProfiles = userManager.getUserProfiles();
+            return userProfiles.get(0) == myHandle ? userProfiles.get(1) : userProfiles.get(0);
+        });
+
+        waitForLauncherCondition("work profile can't be turned off",
+                l -> userManager.requestQuietModeEnabled(true, workProfile));
+
+        assertTrue(userManager.isQuietModeEnabled(workProfile));
+        executeOnLauncher(launcher -> {
+            WorkModeSwitch wf = launcher.getAppsView().getWorkModeSwitch();
+            ((AllAppsPagedView) launcher.getAppsView().getContentView()).snapToPageImmediately(
+                    AllAppsContainerView.AdapterHolder.WORK);
+            wf.toggle();
+        });
+        waitForLauncherCondition("Work toggle did not work",
+                l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));
+    }
+
+    @Test
+    public void testWorkEduFlow() {
+        mDevice.pressHome();
+        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+        executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
+                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);
+
+        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        WorkEduView workEduView = getEduView();
+        // verify personal app edu is seen first and click "next"
+        executeOnLauncher(l -> {
+            assertEquals(((TextView) workEduView.findViewById(R.id.content_text)).getText(),
+                    l.getResources().getString(R.string.work_profile_edu_personal_apps));
+            workEduView.findViewById(R.id.proceed).callOnClick();
+        });
+        // verify work edu is seen next
+        waitForLauncherCondition("Launcher did not show the next edu screen", l ->
+                ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage() == WORK_PAGE
+                        && ((TextView) workEduView.findViewById(
+                        R.id.content_text)).getText().equals(
+                        l.getResources().getString(R.string.work_profile_edu_work_apps)));
+    }
+
+    @Test
+    public void testWorkEduIntermittent() {
+        mDevice.pressHome();
+        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+        executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
+                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);
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
 
-        /*
-        assertTrue("Personal tab is missing", waitForLauncherCondition(
-                launcher -> launcher.getAppsView().isPersonalTabVisible()));
-        assertTrue("Work tab is missing", waitForLauncherCondition(
-                launcher -> launcher.getAppsView().isWorkTabVisible()));
-        */
+        // verify personal app edu is seen
+        getEduView();
+
+        // dismiss personal edu
+        mDevice.pressHome();
+        waitForState("Launcher did not go home", () -> NORMAL);
+
+        // open work tab
+        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        waitForState("Launcher did not switch to all apps", () -> ALL_APPS);
+        executeOnLauncher(launcher -> {
+            AllAppsPagedView pagedView = (AllAppsPagedView) launcher.getAppsView().getContentView();
+            pagedView.setCurrentPage(WORK_PAGE);
+        });
+
+        WorkEduView workEduView = getEduView();
+
+        // verify work tab edu is shown
+        waitForLauncherCondition("Launcher did not show the next edu screen",
+                l -> ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
+                        l.getResources().getString(R.string.work_profile_edu_work_apps)));
     }
+
+
+    private WorkEduView getEduView() {
+        waitForLauncherCondition("Edu did not show", l -> {
+            DragLayer dragLayer = l.getDragLayer();
+            return dragLayer.getChildCount() > 0 && dragLayer.getChildAt(
+                    dragLayer.getChildCount() - 1) instanceof WorkEduView;
+        });
+        return getFromLauncher(launcher -> (WorkEduView) launcher.getDragLayer().getChildAt(
+                launcher.getDragLayer().getChildCount() - 1));
+    }
+
 }
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index e1b3ede..9d4ccff 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,16 +28,16 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.testcomponent.WidgetConfigActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.Wait.Condition;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
 import org.junit.Before;
@@ -94,7 +94,7 @@
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
         widgets.
                 getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())).
-                dragToWorkspace();
+                dragToWorkspace(true, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
 
@@ -103,12 +103,12 @@
 
         setResult(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT);
+            Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
-            Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT);
+            Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
+                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
         }
     }
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index f9d1d93..f146db5 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -23,8 +23,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetInfo;
 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;
@@ -57,7 +57,7 @@
                 getWorkspace().
                 openAllWidgets().
                 getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager())).
-                dragToWorkspace();
+                dragToWorkspace(false, false);
 
         assertTrue(mActivityMonitor.itemExists(
                 (info, view) -> info instanceof LauncherAppWidgetInfo &&
@@ -83,7 +83,7 @@
         mDevice.pressHome();
         mLauncher.getWorkspace().openAllWidgets()
                 .getWidget("com.android.launcher3.testcomponent.CustomShortcutConfigActivity")
-                .dragToWorkspace();
+                .dragToWorkspace(false, true);
         mLauncher.getWorkspace().getWorkspaceAppIcon("Shortcut")
                 .launch(getAppPackageName());
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index f42bf1f..a4aa9f2 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -15,13 +15,15 @@
  */
 package com.android.launcher3.ui.widget;
 
-import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static com.android.launcher3.common.WidgetUtils.createWidgetInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -30,24 +32,22 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.widget.RemoteViews;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.WidgetHostViewLoader;
+import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
 import org.junit.Before;
@@ -57,7 +57,6 @@
 
 import java.util.HashSet;
 import java.util.Set;
-import java.util.function.Consumer;
 
 /**
  * Tests for bind widget flow.
@@ -72,7 +71,6 @@
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private ContentResolver mResolver;
-    private AppWidgetManagerCompat mWidgetManager;
 
     // Objects created during test, which should be cleaned up in the end.
     private Cursor mCursor;
@@ -85,11 +83,11 @@
         super.setUp();
 
         mResolver = mTargetContext.getContentResolver();
-        mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext);
 
         // Clear all existing data
         LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+        LauncherSettings.Settings.call(mResolver,
+                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
     }
 
     @After
@@ -106,28 +104,28 @@
     @Test
     public void testBindNormalWidget_withConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyWidgetPresent(info);
     }
 
     @Test
     public void testBindNormalWidget_withoutConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyWidgetPresent(info);
     }
 
     @Test
     public void testUnboundWidget_removed() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.appWidgetId = -33;
 
-        setupContents(item);
+        addItemToScreen(item);
 
         final Workspace workspace = mLauncher.getWorkspace();
         // Item deleted from db
@@ -145,10 +143,10 @@
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
 
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyWidgetPresent(info);
     }
 
@@ -158,10 +156,10 @@
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
 
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyPendingWidgetPresent();
 
         // Item deleted from db
@@ -175,6 +173,26 @@
         assertNotNull(AppWidgetManager.getInstance(mTargetContext)
                 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
                         LauncherSettings.Favorites.APPWIDGET_ID))));
+
+        // send OPTION_APPWIDGET_RESTORE_COMPLETED
+        int appWidgetId = mCursor.getInt(
+                mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID));
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+
+        Bundle b = new Bundle();
+        b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true);
+        RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready);
+        appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
+        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
+
+
+        // verify changes are reflected
+        waitForLauncherCondition("App widget options did not update",
+                l -> appWidgetManager.getAppWidgetOptions(appWidgetId).getBoolean(
+                        WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
+        executeOnLauncher(l -> l.getAppWidgetHost().startListening());
+        verifyWidgetPresent(info);
+        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT));
     }
 
     @Test
@@ -183,7 +201,7 @@
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
-        setupContents(item);
+        addItemToScreen(item);
 
         assertTrue("Pending widget exists",
                 mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
@@ -202,7 +220,7 @@
                 | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyPendingWidgetPresent();
 
         // Verify item still exists in db
@@ -230,7 +248,7 @@
         PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
         mSessionId = installer.createSession(params);
 
-        setupContents(item);
+        addItemToScreen(item);
         verifyPendingWidgetPresent();
 
         // Verify item still exists in db
@@ -245,79 +263,16 @@
                         & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
     }
 
-    /**
-     * Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the
-     * widget class is displayed on the homescreen.
-     */
-    private void setupContents(LauncherAppWidgetInfo item) {
-        int screenId = FIRST_SCREEN_ID;
-        // Update the screen id counter for the provider.
-        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
-
-        if (screenId > FIRST_SCREEN_ID) {
-            screenId = FIRST_SCREEN_ID;
-        }
-
-        // Insert the item
-        ContentWriter writer = new ContentWriter(mTargetContext);
-        item.id = LauncherSettings.Settings.call(
-                mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-        item.screenId = screenId;
-        item.onAddToDatabase(writer);
-        writer.put(LauncherSettings.Favorites._ID, item.id);
-        mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, writer.getValues(mTargetContext));
-        resetLoaderState();
-
-        // Launch the home activity
-        mDevice.pressHome();
-    }
-
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
+        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT);
         assertTrue("Widget is not present",
-                mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
+                widget != null);
     }
 
     private void verifyPendingWidgetPresent() {
+        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT);
         assertTrue("Pending widget is not present",
-                mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT) != null);
-    }
-
-    /**
-     * Creates a LauncherAppWidgetInfo corresponding to {@param info}
-     * @param bindWidget if true the info is bound and a valid widgetId is assigned to
-     *                   the LauncherAppWidgetInfo
-     */
-    private LauncherAppWidgetInfo createWidgetInfo(
-            LauncherAppWidgetProviderInfo info, boolean bindWidget) {
-        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
-                LauncherAppWidgetInfo.NO_ID, info.provider);
-        item.spanX = info.minSpanX;
-        item.spanY = info.minSpanY;
-        item.minSpanX = info.minSpanX;
-        item.minSpanY = info.minSpanY;
-        item.user = info.getProfile();
-        item.cellX = 0;
-        item.cellY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-
-        if (bindWidget) {
-            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
-            pendingInfo.spanX = item.spanX;
-            pendingInfo.spanY = item.spanY;
-            pendingInfo.minSpanX = item.minSpanX;
-            pendingInfo.minSpanY = item.minSpanY;
-            Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(mTargetContext, pendingInfo);
-
-            AppWidgetHost host = new LauncherAppWidgetHost(mTargetContext);
-            int widgetId = host.allocateAppWidgetId();
-            if (!mWidgetManager.bindAppWidgetIdIfAllowed(widgetId, info, options)) {
-                host.deleteAppWidgetId(widgetId);
-                throw new IllegalArgumentException("Unable to bind widget id");
-            }
-            item.appWidgetId = widgetId;
-        }
-        return item;
+                widget != null);
     }
 
     /**
@@ -330,11 +285,11 @@
 
         Set<String> activePackage = getOnUiThread(() -> {
             Set<String> packages = new HashSet<>();
-            PackageInstallerCompat.getInstance(mTargetContext).updateAndGetActiveSessionCache()
+            InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions()
                     .keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName));
             return packages;
         });
-        while(true) {
+        while (true) {
             try {
                 mTargetContext.getPackageManager().getPackageInfo(
                         pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
@@ -344,7 +299,7 @@
                 }
             }
             pkg = invalidPackage + count;
-            count ++;
+            count++;
         }
         LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10,
                 new ComponentName(pkg, "com.test.widgetprovider"));
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 07129dd..0e43d81 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -29,20 +29,20 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace.ItemOperator;
-import com.android.launcher3.WorkspaceItemInfo;
+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.shortcuts.ShortcutKey;
 import com.android.launcher3.tapl.AddToHomeScreenPrompt;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
 import com.android.launcher3.testcomponent.RequestPinItemActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.Wait.Condition;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
 import org.junit.Before;
@@ -59,7 +59,8 @@
 @RunWith(AndroidJUnit4.class)
 public class RequestPinItemTest extends AbstractLauncherUiTest {
 
-    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+    @Rule
+    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private String mCallbackAction;
     private String mShortcutId;
@@ -84,10 +85,10 @@
                         .equals(AppWidgetNoConfig.class.getName()));
     }
 
-        @Test
+    @Test
     public void testPinWidgetNoConfig_customPreview() throws Throwable {
         // Command to set custom preview
-        Intent command =  RequestPinItemActivity.getCommandIntent(
+        Intent command = RequestPinItemActivity.getCommandIntent(
                 RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
                 RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
 
@@ -169,7 +170,8 @@
 
         // Go back to home
         mLauncher.pressHome();
-        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT);
+        Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
+                mLauncher);
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/util/Condition.java b/tests/src/com/android/launcher3/util/Condition.java
deleted file mode 100644
index d85dd3a..0000000
--- a/tests/src/com/android/launcher3/util/Condition.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.android.launcher3.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import androidx.test.uiautomator.UiObject2;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public interface Condition {
-
-    boolean isTrue() throws Throwable;
-
-    /**
-     * Converts the condition to be run on UI thread.
-     */
-    static Condition runOnUiThread(final Condition condition) {
-        final LooperExecutor executor = MAIN_EXECUTOR;
-        return () -> {
-            final AtomicBoolean value = new AtomicBoolean(false);
-            final Throwable[] exceptions = new Throwable[1];
-            final CountDownLatch latch = new CountDownLatch(1);
-            executor.execute(() -> {
-                try {
-                    value.set(condition.isTrue());
-                } catch (Throwable e) {
-                    exceptions[0] = e;
-                }
-
-            });
-            latch.await(1, TimeUnit.SECONDS);
-            if (exceptions[0] != null) {
-                throw exceptions[0];
-            }
-            return value.get();
-        };
-    }
-
-    static Condition minChildCount(final UiObject2 obj, final int childCount) {
-        return () -> obj.getChildCount() >= childCount;
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
index 8f89173..ed2ec7b 100644
--- a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.util;
 
 import static com.android.launcher3.util.Executors.createAndStartNewLooper;
-import static com.android.launcher3.util.RaceConditionTracker.ENTER_POSTFIX;
-import static com.android.launcher3.util.RaceConditionTracker.EXIT_POSTFIX;
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -64,15 +62,30 @@
  *
  * When we register event XXX:enter, we hold all other events until we register XXX:exit.
  */
-public class RaceConditionReproducer implements RaceConditionTracker.EventProcessor {
+public class RaceConditionReproducer {
     private static final String TAG = "RaceConditionReproducer";
+
+    private static final boolean ENTER = true;
+    private static final boolean EXIT = false;
+    private static final String ENTER_POSTFIX = "enter";
+    private static final String EXIT_POSTFIX = "exit";
+
     private static final long SHORT_TIMEOUT_MS = 2000;
     private static final long LONG_TIMEOUT_MS = 60000;
     // Handler used to resume postponed events.
-    private static final Handler POSTPONED_EVENT_RESUME_HANDLER = createEventResumeHandler();
+    private static final Handler POSTPONED_EVENT_RESUME_HANDLER =
+            new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
 
-    private static Handler createEventResumeHandler() {
-        return new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
+    public static String enterExitEvt(String eventName, boolean isEnter) {
+        return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX);
+    }
+
+    public static String enterEvt(String eventName) {
+        return enterExitEvt(eventName, ENTER);
+    }
+
+    public static String exitEvt(String eventName) {
+        return enterExitEvt(eventName, EXIT);
     }
 
     /**
@@ -209,7 +222,8 @@
                 parseReproString(mReproString) : generateSequenceToFollowLocked();
         Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
         checkIfCompletedSequenceToFollowLocked();
-        RaceConditionTracker.setEventProcessor(this);
+
+        TraceHelperForTest.setRaceConditionReproducer(this);
     }
 
     /**
@@ -218,7 +232,8 @@
      * Returns whether we need more iterations.
      */
     public synchronized boolean finishIteration() {
-        RaceConditionTracker.setEventProcessor(null);
+        TraceHelperForTest.setRaceConditionReproducer(null);
+
         runResumeAllEventsCallbackLocked();
         assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
         assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
@@ -243,7 +258,6 @@
     /**
      * Called when the app issues an event.
      */
-    @Override
     public void onEvent(String event) {
         final Semaphore waitObject = tryRegisterEvent(event);
         if (waitObject != null) {
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
index 3fc268e..59f2173 100644
--- a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
@@ -22,6 +22,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,17 +39,29 @@
         return res;
     }
 
-    private static void run3_3_TestAction() throws InterruptedException {
+    RaceConditionReproducer eventProcessor;
+
+    @Before
+    public void setup() {
+        eventProcessor = new RaceConditionReproducer();
+    }
+
+    @After
+    public void tearDown() {
+        TraceHelperForTest.cleanup();
+    }
+
+    private void run3_3_TestAction() throws InterruptedException {
         Thread tb = new Thread(() -> {
-            RaceConditionTracker.onEvent("B1");
-            RaceConditionTracker.onEvent("B2");
-            RaceConditionTracker.onEvent("B3");
+            eventProcessor.onEvent("B1");
+            eventProcessor.onEvent("B2");
+            eventProcessor.onEvent("B3");
         });
         tb.start();
 
-        RaceConditionTracker.onEvent("A1");
-        RaceConditionTracker.onEvent("A2");
-        RaceConditionTracker.onEvent("A3");
+        eventProcessor.onEvent("A1");
+        eventProcessor.onEvent("A2");
+        eventProcessor.onEvent("A3");
 
         tb.join();
     }
@@ -56,7 +70,6 @@
     @Ignore // The test is too long for continuous testing.
     // 2 threads, 3 events each.
     public void test3_3() throws Exception {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
         boolean sawTheValidSequence = false;
 
         for (; ; ) {
@@ -80,25 +93,24 @@
     @Ignore // The test is too long for continuous testing.
     // 2 threads, 3 events, including enter-exit pairs each.
     public void test3_3_enter_exit() throws Exception {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
         boolean sawTheValidSequence = false;
 
         for (; ; ) {
             eventProcessor.startIteration();
             Thread tb = new Thread(() -> {
-                RaceConditionTracker.onEvent("B1:enter");
-                RaceConditionTracker.onEvent("B1:exit");
-                RaceConditionTracker.onEvent("B2");
-                RaceConditionTracker.onEvent("B3:enter");
-                RaceConditionTracker.onEvent("B3:exit");
+                eventProcessor.onEvent("B1:enter");
+                eventProcessor.onEvent("B1:exit");
+                eventProcessor.onEvent("B2");
+                eventProcessor.onEvent("B3:enter");
+                eventProcessor.onEvent("B3:exit");
             });
             tb.start();
 
-            RaceConditionTracker.onEvent("A1");
-            RaceConditionTracker.onEvent("A2:enter");
-            RaceConditionTracker.onEvent("A2:exit");
-            RaceConditionTracker.onEvent("A3:enter");
-            RaceConditionTracker.onEvent("A3:exit");
+            eventProcessor.onEvent("A1");
+            eventProcessor.onEvent("A2:enter");
+            eventProcessor.onEvent("A2:exit");
+            eventProcessor.onEvent("A3:enter");
+            eventProcessor.onEvent("A3:exit");
 
             tb.join();
             final boolean needMoreIterations = eventProcessor.finishIteration();
@@ -119,9 +131,7 @@
     @Test
     // 2 threads, 3 events each; reproducing a particular event sequence.
     public void test3_3_ReproMode() throws Exception {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(
-                SOME_VALID_SEQUENCE_3_3);
-
+        eventProcessor = new RaceConditionReproducer(SOME_VALID_SEQUENCE_3_3);
         eventProcessor.startIteration();
         run3_3_TestAction();
         assertTrue(!eventProcessor.finishIteration());
@@ -134,23 +144,21 @@
     @Ignore // The test is too long for continuous testing.
     // 2 threads with 2 events; 1 thread with 1 event.
     public void test2_1_2() throws Exception {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
-
         for (; ; ) {
             eventProcessor.startIteration();
             Thread tb = new Thread(() -> {
-                RaceConditionTracker.onEvent("B1");
-                RaceConditionTracker.onEvent("B2");
+                eventProcessor.onEvent("B1");
+                eventProcessor.onEvent("B2");
             });
             tb.start();
 
             Thread tc = new Thread(() -> {
-                RaceConditionTracker.onEvent("C1");
+                eventProcessor.onEvent("C1");
             });
             tc.start();
 
-            RaceConditionTracker.onEvent("A1");
-            RaceConditionTracker.onEvent("A2");
+            eventProcessor.onEvent("A1");
+            eventProcessor.onEvent("A2");
 
             tb.join();
             tc.join();
@@ -167,28 +175,26 @@
     @Ignore // The test is too long for continuous testing.
     // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
     public void test2_1_2_enter_exit() throws Exception {
-        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
-
         for (; ; ) {
             eventProcessor.startIteration();
             Thread tb = new Thread(() -> {
-                RaceConditionTracker.onEvent("B1:enter");
-                RaceConditionTracker.onEvent("B1:exit");
-                RaceConditionTracker.onEvent("B2:enter");
-                RaceConditionTracker.onEvent("B2:exit");
+                eventProcessor.onEvent("B1:enter");
+                eventProcessor.onEvent("B1:exit");
+                eventProcessor.onEvent("B2:enter");
+                eventProcessor.onEvent("B2:exit");
             });
             tb.start();
 
             Thread tc = new Thread(() -> {
-                RaceConditionTracker.onEvent("C1:enter");
-                RaceConditionTracker.onEvent("C1:exit");
+                eventProcessor.onEvent("C1:enter");
+                eventProcessor.onEvent("C1:exit");
             });
             tc.start();
 
-            RaceConditionTracker.onEvent("A1:enter");
-            RaceConditionTracker.onEvent("A1:exit");
-            RaceConditionTracker.onEvent("A2:enter");
-            RaceConditionTracker.onEvent("A2:exit");
+            eventProcessor.onEvent("A1:enter");
+            eventProcessor.onEvent("A1:exit");
+            eventProcessor.onEvent("A2:enter");
+            eventProcessor.onEvent("A2:exit");
 
             tb.join();
             tc.join();
diff --git a/tests/src/com/android/launcher3/util/TraceHelperForTest.java b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
new file mode 100644
index 0000000..f1c8a67
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import java.util.LinkedList;
+import java.util.function.IntConsumer;
+
+public class TraceHelperForTest extends TraceHelper {
+
+    private static final TraceHelperForTest INSTANCE_FOR_TEST = new TraceHelperForTest();
+
+    private final ThreadLocal<LinkedList<TraceInfo>> mStack =
+            ThreadLocal.withInitial(LinkedList::new);
+
+    private RaceConditionReproducer mRaceConditionReproducer;
+    private IntConsumer mFlagsChangeListener;
+
+    public static void setRaceConditionReproducer(RaceConditionReproducer reproducer) {
+        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+        INSTANCE_FOR_TEST.mRaceConditionReproducer = reproducer;
+    }
+
+    public static void cleanup() {
+        INSTANCE_FOR_TEST.mRaceConditionReproducer = null;
+        INSTANCE_FOR_TEST.mFlagsChangeListener = null;
+    }
+
+    public static void setFlagsChangeListener(IntConsumer listener) {
+        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+        INSTANCE_FOR_TEST.mFlagsChangeListener = listener;
+    }
+
+    private TraceHelperForTest() { }
+
+    @Override
+    public Object beginSection(String sectionName, int flags) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = new TraceInfo(sectionName, flags);
+        stack.add(info);
+
+        if ((flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+                 && mRaceConditionReproducer != null) {
+            mRaceConditionReproducer.onEvent(RaceConditionReproducer.enterEvt(sectionName));
+        }
+        updateBinderTracking(stack);
+
+        super.beginSection(sectionName, flags);
+        return info;
+    }
+
+    @Override
+    public void endSection(Object token) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        if (stack.size() == 0) {
+            new Throwable().printStackTrace();
+        }
+        TraceInfo info = (TraceInfo) token;
+        stack.remove(info);
+        if ((info.flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+                && mRaceConditionReproducer != null) {
+            mRaceConditionReproducer.onEvent(RaceConditionReproducer.exitEvt(info.sectionName));
+        }
+        updateBinderTracking(stack);
+
+        super.endSection(token);
+    }
+
+    @Override
+    public Object beginFlagsOverride(int flags) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = new TraceInfo(null, flags);
+        stack.add(info);
+        updateBinderTracking(stack);
+        super.beginFlagsOverride(flags);
+        return info;
+    }
+
+    @Override
+    public void endFlagsOverride(Object token) {
+        super.endFlagsOverride(token);
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = (TraceInfo) token;
+        stack.remove(info);
+        updateBinderTracking(stack);
+    }
+
+    private void updateBinderTracking(LinkedList<TraceInfo> stack) {
+        if (mFlagsChangeListener != null) {
+            mFlagsChangeListener.accept(stack.stream()
+                    .mapToInt(info -> info.flags).reduce(0, (a, b) -> a | b));
+        }
+    }
+
+    private static class TraceInfo {
+        public final String sectionName;
+        public final int flags;
+
+        TraceInfo(String sectionName, int flags) {
+            this.sectionName = sectionName;
+            this.flags = flags;
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index 899686b..fe6143c 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -3,8 +3,12 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.launcher3.tapl.LauncherInstrumentation;
+
 import org.junit.Assert;
 
+import java.util.function.Supplier;
+
 /**
  * A utility class for waiting for a condition to be true.
  */
@@ -12,11 +16,19 @@
 
     private static final long DEFAULT_SLEEP_MS = 200;
 
-    public static void atMost(String message, Condition condition, long timeout) {
-        atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
+    public static void atMost(String message, Condition condition, long timeout,
+            LauncherInstrumentation launcher) {
+        atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
     }
 
-    public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
+    public static void atMost(Supplier<String> message, Condition condition, long timeout,
+            LauncherInstrumentation launcher) {
+        atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
+    }
+
+    public static void atMost(Supplier<String> message, Condition condition, long timeout,
+            long sleepMillis,
+            LauncherInstrumentation launcher) {
         final long startTime = SystemClock.uptimeMillis();
         long endTime = startTime + timeout;
         Log.d("Wait", "atMost: " + startTime + " - " + endTime);
@@ -40,6 +52,15 @@
             throw new RuntimeException(t);
         }
         Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
-        Assert.fail(message);
+        launcher.checkForAnomaly();
+        Assert.fail(message.get());
+    }
+
+    /**
+     * Interface representing a generic condition
+     */
+    public interface Condition {
+
+        boolean isTrue() throws Throwable;
     }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
new file mode 100644
index 0000000..e4f520f
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import android.os.SystemClock;
+
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+class FailureInvestigator {
+    private static boolean matches(String regex, CharSequence string) {
+        return Pattern.compile(regex).matcher(string).find();
+    }
+
+    static class LogcatMatch {
+        String logcatPattern;
+        int bug;
+
+        LogcatMatch(String logcatPattern, int bug) {
+            this.logcatPattern = logcatPattern;
+            this.bug = bug;
+        }
+    }
+
+    static class ExceptionMatch {
+        String exceptionPattern;
+        LogcatMatch[] logcatMatches;
+
+        ExceptionMatch(String exceptionPattern, LogcatMatch[] logcatMatches) {
+            this.exceptionPattern = exceptionPattern;
+            this.logcatMatches = logcatMatches;
+        }
+    }
+
+    private static final ExceptionMatch[] EXCEPTION_MATCHES = {
+            new ExceptionMatch(
+                    "java.lang.AssertionError: http://go/tapl : Tests are broken by a "
+                            + "non-Launcher system error: (Phone is locked|Screen is empty)",
+                    new LogcatMatch[]{
+                            new LogcatMatch(
+                                    "BroadcastQueue: Can't deliver broadcast to com.android"
+                                            + ".systemui.*Crashing it",
+                                    147845913),
+                            new LogcatMatch(
+                                    "Attempt to invoke virtual method 'boolean android\\"
+                                            + ".graphics\\.Bitmap\\.isRecycled\\(\\)' on a null "
+                                            + "object reference",
+                                    148424291),
+                            new LogcatMatch(
+                                    "java\\.lang\\.IllegalArgumentException\\: Ranking map "
+                                            + "doesn't contain key",
+                                    148570537),
+                    }),
+            new ExceptionMatch("Launcher didn't initialize",
+                    new LogcatMatch[]{
+                            new LogcatMatch(
+                                    "ActivityManager: Reason: executing service com.google"
+                                            + ".android.apps.nexuslauncher/com.android.launcher3"
+                                            + ".notification.NotificationListener",
+                                    148238677),
+                    }),
+    };
+
+    static int getBugForFailure(CharSequence exception) {
+        if ("com.google.android.setupwizard".equals(
+                UiDevice.getInstance(getInstrumentation()).getLauncherPackageName())) {
+            return 145935261;
+        }
+
+        if (matches("java\\.lang\\.AssertionError\\: http\\:\\/\\/go\\/tapl \\: want to get "
+                + "workspace object; Presence of recents button doesn't match the interaction "
+                + "mode, mode\\=ZERO_BUTTON, hasRecents\\=true", exception)) {
+            return 148422894;
+        }
+
+        final String logSinceBoot;
+        try {
+            final String systemBootTime =
+                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(
+                            new Date(System.currentTimeMillis() - SystemClock.elapsedRealtime()));
+
+            logSinceBoot =
+                    UiDevice.getInstance(getInstrumentation())
+                            .executeShellCommand("logcat -d -t " + systemBootTime.replace(" ", ""));
+        } catch (IOException | OutOfMemoryError e) {
+            return 0;
+        }
+
+        if (matches("android\\:\\:uirenderer\\:\\:renderthread\\:\\:EglManager\\:\\:swapBuffers",
+                logSinceBoot)) {
+            return 148529608;
+        }
+
+        for (ExceptionMatch exceptionMatch : EXCEPTION_MATCHES) {
+            if (matches(exceptionMatch.exceptionPattern, exception)) {
+                for (LogcatMatch logcatMatch : exceptionMatch.logcatMatches) {
+                    if (matches(logcatMatch.logcatPattern, logSinceBoot)) {
+                        return logcatMatch.bug;
+                    }
+                }
+                break;
+            }
+        }
+
+        return 0;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java b/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java
new file mode 100644
index 0000000..99ddee4
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.rule;
+
+import android.util.Log;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class FailureRewriterRule implements TestRule {
+    private static final String TAG = "FailureRewriter";
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    base.evaluate();
+                } catch (Throwable e) {
+                    final int bug = FailureInvestigator.getBugForFailure(e.toString());
+                    if (bug == 0) throw e;
+
+                    Log.e(TAG, "Known bug found for the original failure "
+                            + android.util.Log.getStackTraceString(e));
+                    throw new AssertionError(
+                            "Detected a failure that matches a known bug b/" + bug);
+                }
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
index 62fe26d..6a6ec3e 100644
--- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
@@ -16,16 +16,10 @@
 package com.android.launcher3.util.rule;
 
 import android.app.Activity;
-import android.app.Application;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.os.Bundle;
-
-import androidx.test.InstrumentationRegistry;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Workspace.ItemOperator;
 
-import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
@@ -34,17 +28,23 @@
 /**
  * Test rule to get the current Launcher activity.
  */
-public class LauncherActivityRule implements TestRule {
+public class LauncherActivityRule extends SimpleActivityRule<Launcher> {
 
-    private Launcher mActivity;
+    public LauncherActivityRule() {
+        super(Launcher.class);
+    }
 
     @Override
     public Statement apply(Statement base, Description description) {
-        return new MyStatement(base);
-    }
 
-    public Launcher getActivity() {
-        return mActivity;
+        return new MyStatement(base) {
+            @Override
+            public void onActivityStarted(Activity activity) {
+                if (activity instanceof Launcher) {
+                    ((Launcher) activity).getRotationHelper().forceAllowRotationForTesting(true);
+                }
+            }
+        };
     }
 
     public Callable<Boolean> itemExists(final ItemOperator op) {
@@ -56,62 +56,4 @@
             return launcher.getWorkspace().getFirstMatch(op) != null;
         };
     }
-
-    private class MyStatement extends Statement implements ActivityLifecycleCallbacks {
-
-        private final Statement mBase;
-
-        public MyStatement(Statement base) {
-            mBase = base;
-        }
-
-        @Override
-        public void evaluate() throws Throwable {
-            Application app = (Application)
-                    InstrumentationRegistry.getTargetContext().getApplicationContext();
-            app.registerActivityLifecycleCallbacks(this);
-            try {
-                mBase.evaluate();
-            } finally {
-                app.unregisterActivityLifecycleCallbacks(this);
-            }
-        }
-
-        @Override
-        public void onActivityCreated(Activity activity, Bundle bundle) {
-            if (activity instanceof Launcher) {
-                mActivity = (Launcher) activity;
-            }
-        }
-
-        @Override
-        public void onActivityStarted(Activity activity) {
-            if (activity instanceof Launcher) {
-                mActivity.getRotationHelper().forceAllowRotationForTesting(true);
-            }
-        }
-
-        @Override
-        public void onActivityResumed(Activity activity) {
-        }
-
-        @Override
-        public void onActivityPaused(Activity activity) {
-        }
-
-        @Override
-        public void onActivityStopped(Activity activity) {
-        }
-
-        @Override
-        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityDestroyed(Activity activity) {
-            if (activity == mActivity) {
-                mActivity = null;
-            }
-        }
-    }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
new file mode 100644
index 0000000..1dbba6a
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/SimpleActivityRule.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.launcher3.util.rule;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Test rule to get the current activity.
+ */
+public class SimpleActivityRule<T extends Activity> implements TestRule {
+
+    private final Class<T> mClass;
+    private T mActivity;
+
+    public SimpleActivityRule(Class<T> clazz) {
+        mClass = clazz;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new MyStatement(base);
+    }
+
+    public T getActivity() {
+        return mActivity;
+    }
+
+    protected class MyStatement extends Statement implements ActivityLifecycleCallbacks {
+
+        private final Statement mBase;
+
+        public MyStatement(Statement base) {
+            mBase = base;
+        }
+
+        @Override
+        public void evaluate() throws Throwable {
+            Application app = (Application)
+                    InstrumentationRegistry.getTargetContext().getApplicationContext();
+            app.registerActivityLifecycleCallbacks(this);
+            try {
+                mBase.evaluate();
+            } finally {
+                app.unregisterActivityLifecycleCallbacks(this);
+                mActivity = null;
+            }
+        }
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle bundle) {
+            if (activity != null && mClass.isInstance(activity)) {
+                mActivity = (T) activity;
+            }
+        }
+
+        @Override
+        public void onActivityStarted(Activity activity) {
+        }
+
+        @Override
+        public void onActivityResumed(Activity activity) {
+        }
+
+        @Override
+        public void onActivityPaused(Activity activity) {
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+        }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
+        }
+
+        @Override
+        public void onActivityDestroyed(Activity activity) {
+            if (activity == mActivity) {
+                mActivity = null;
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index d7f41bf..61f5e05 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -17,9 +17,11 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import android.content.pm.PackageManager;
 import android.os.Build;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.rules.TestRule;
@@ -50,20 +52,33 @@
                     + "(?<postsubmit>[0-9]+)"
                     + ")$");
 
+    public static final int LOCAL = 0x1;
+    public static final int UNBUNDLED_PRESUBMIT = 0x2;
+    public static final int UNBUNDLED_POSTSUBMIT = 0x4;
+    public static final int PLATFORM_PRESUBMIT = 0x8;
+    public static final int PLATFORM_POSTSUBMIT = 0x10;
+
+    private static int sRunFlavor;
+
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
     public @interface Stability {
+        int flavors();
     }
 
     @Override
     public Statement apply(Statement base, Description description) {
-        if (description.getAnnotation(Stability.class) != null) {
+        final Stability stability = description.getAnnotation(Stability.class);
+        if (stability != null) {
             return new Statement() {
                 @Override
                 public void evaluate() throws Throwable {
-                    getRunFlavor();
-
-                    base.evaluate();
+                    if ((stability.flavors() & getRunFlavor()) != 0) {
+                        Log.d(TAG, "Running " + description.getDisplayName());
+                        base.evaluate();
+                    } else {
+                        Log.d(TAG, "Skipping " + description.getDisplayName());
+                    }
                 }
             };
         } else {
@@ -71,49 +86,77 @@
         }
     }
 
-    private static void getRunFlavor() throws Exception {
-        final String launcherVersion = getInstrumentation().
-                getContext().
-                getPackageManager().
-                getPackageInfo(
-                        UiDevice.getInstance(getInstrumentation()).
-                                getLauncherPackageName(),
-                        0).
-                versionName;
+    public static int getRunFlavor() {
+        if (sRunFlavor != 0) return sRunFlavor;
 
-        final Matcher launcherBuildMatcher = LAUNCHER_BUILD.matcher(launcherVersion);
+        final String flavorOverride = InstrumentationRegistry.getArguments().getString("flavor");
 
-        if (!launcherBuildMatcher.find()) {
-            Log.e(TAG, "Match not found");
+        if (flavorOverride != null) {
+            Log.d(TAG, "Flavor override: " + flavorOverride);
+            try {
+                return (int) TestStabilityRule.class.getField(flavorOverride).get(null);
+            } catch (NoSuchFieldException e) {
+                throw new AssertionError("Unrecognized run flavor override: " + flavorOverride);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        final String launcherVersion;
+        try {
+            final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
+                    .getLauncherPackageName();
+            Log.d(TAG, "Launcher package: " + launcherPackageName);
+
+            launcherVersion = getInstrumentation().
+                    getContext().
+                    getPackageManager().
+                    getPackageInfo(launcherPackageName, 0)
+                    .versionName;
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
         }
 
         final String platformVersion = Build.VERSION.INCREMENTAL;
-        final Matcher platformBuildMatcher = PLATFORM_BUILD.matcher(platformVersion);
-
-        if (!platformBuildMatcher.find()) {
-            Log.e(TAG, "Match not found");
-        }
 
         Log.d(TAG, "Launcher: " + launcherVersion + ", platform: " + platformVersion);
 
+        final Matcher launcherBuildMatcher = LAUNCHER_BUILD.matcher(launcherVersion);
+        if (!launcherBuildMatcher.find()) {
+            throw new AssertionError("Launcher build match not found");
+        }
+
+        final Matcher platformBuildMatcher = PLATFORM_BUILD.matcher(platformVersion);
+        if (!platformBuildMatcher.find()) {
+            throw new AssertionError("Platform build match not found");
+        }
+
         if (launcherBuildMatcher.group("local") != null && (
                 platformBuildMatcher.group("commandLine") != null ||
                         platformBuildMatcher.group("postsubmit") != null)) {
             Log.d(TAG, "LOCAL RUN");
+            sRunFlavor = LOCAL;
         } else if (launcherBuildMatcher.group("presubmit") != null
                 && platformBuildMatcher.group("postsubmit") != null) {
             Log.d(TAG, "UNBUNDLED PRESUBMIT");
+            sRunFlavor = UNBUNDLED_PRESUBMIT;
         } else if (launcherBuildMatcher.group("postsubmit") != null
                 && platformBuildMatcher.group("postsubmit") != null) {
             Log.d(TAG, "UNBUNDLED POSTSUBMIT");
+            sRunFlavor = UNBUNDLED_POSTSUBMIT;
         } else if (launcherBuildMatcher.group("platform") != null
                 && platformBuildMatcher.group("presubmit") != null) {
             Log.d(TAG, "PLATFORM PRESUBMIT");
+            sRunFlavor = PLATFORM_PRESUBMIT;
         } else if (launcherBuildMatcher.group("platform") != null
-                && platformBuildMatcher.group("postsubmit") != null) {
+                && (platformBuildMatcher.group("postsubmit") != null
+                || platformBuildMatcher.group("commandLine") != null)) {
             Log.d(TAG, "PLATFORM POSTSUBMIT");
+            sRunFlavor = PLATFORM_POSTSUBMIT;
         } else {
-            Log.e(TAG, "ERROR3");
+            throw new AssertionError("Unrecognized run flavor");
         }
+
+        return sRunFlavor;
     }
 }
diff --git a/tests/src_common/README.md b/tests/src_common/README.md
new file mode 100644
index 0000000..2bc9e73
--- /dev/null
+++ b/tests/src_common/README.md
@@ -0,0 +1 @@
+Common source code used by both android tests and robolectric tests
\ No newline at end of file
diff --git a/tests/src_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
new file mode 100644
index 0000000..c0913bf
--- /dev/null
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -0,0 +1,104 @@
+/*
+ * 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.common;
+
+import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
+import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
+
+import android.appwidget.AppWidgetHost;
+import android.content.ContentResolver;
+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.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetManagerHelper;
+
+/**
+ * Common method for widget binding
+ */
+public class WidgetUtils {
+
+    /**
+     * Creates a LauncherAppWidgetInfo corresponding to {@param info}
+     *
+     * @param bindWidget if true the info is bound and a valid widgetId is assigned to
+     *                   the LauncherAppWidgetInfo
+     */
+    public static LauncherAppWidgetInfo createWidgetInfo(
+            LauncherAppWidgetProviderInfo info, Context targetContext, boolean bindWidget) {
+        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
+                LauncherAppWidgetInfo.NO_ID, info.provider);
+        item.spanX = info.minSpanX;
+        item.spanY = info.minSpanY;
+        item.minSpanX = info.minSpanX;
+        item.minSpanY = info.minSpanY;
+        item.user = info.getProfile();
+        item.cellX = 0;
+        item.cellY = 1;
+        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+
+        if (bindWidget) {
+            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
+            pendingInfo.spanX = item.spanX;
+            pendingInfo.spanY = item.spanY;
+            pendingInfo.minSpanX = item.minSpanX;
+            pendingInfo.minSpanY = item.minSpanY;
+            Bundle options = getDefaultOptionsForWidget(targetContext, pendingInfo);
+
+            AppWidgetHost host = new LauncherAppWidgetHost(targetContext);
+            int widgetId = host.allocateAppWidgetId();
+            if (!new WidgetManagerHelper(targetContext)
+                    .bindAppWidgetIdIfAllowed(widgetId, info, options)) {
+                host.deleteAppWidgetId(widgetId);
+                throw new IllegalArgumentException("Unable to bind widget id");
+            }
+            item.appWidgetId = widgetId;
+        }
+        return item;
+    }
+
+    /**
+     * Adds {@param item} on the homescreen on the 0th screen
+     */
+    public static void addItemToScreen(ItemInfo item, Context targetContext) {
+        ContentResolver resolver = targetContext.getContentResolver();
+        int screenId = FIRST_SCREEN_ID;
+        // Update the screen id counter for the provider.
+        LauncherSettings.Settings.call(resolver,
+                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+        if (screenId > FIRST_SCREEN_ID) {
+            screenId = FIRST_SCREEN_ID;
+        }
+
+        // Insert the item
+        ContentWriter writer = new ContentWriter(targetContext);
+        item.id = LauncherSettings.Settings.call(
+                resolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        item.screenId = screenId;
+        item.onAddToDatabase(writer);
+        writer.put(LauncherSettings.Favorites._ID, item.id);
+        resolver.insert(LauncherSettings.Favorites.CONTENT_URI,
+                writer.getValues(targetContext));
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 03d1600..e1fde3b 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -19,8 +19,11 @@
 import static java.util.regex.Pattern.CASE_INSENSITIVE;
 
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
 import java.util.regex.Pattern;
 
 public class AddToHomeScreenPrompt {
@@ -31,14 +34,30 @@
 
     AddToHomeScreenPrompt(LauncherInstrumentation launcher) {
         mLauncher = launcher;
-        mWidgetCell = launcher.waitForLauncherObject(By.clazz(
-                "com.android.launcher3.widget.WidgetCell"));
+        mWidgetCell = launcher.waitForLauncherObject(getSelector());
         mLauncher.assertNotNull("Can't find widget cell object", mWidgetCell);
     }
 
+    private static BySelector getSelector() {
+        return By.clazz("com.android.launcher3.widget.WidgetCell");
+    }
+
     public void addAutomatically() {
-        mLauncher.waitForObjectInContainer(
-                mWidgetCell.getParent().getParent().getParent().getParent(),
-                By.text(ADD_AUTOMATICALLY)).click();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            if (mLauncher.getNavigationModel()
+                    != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+                if (!mLauncher.isLauncher3()) {
+                    mLauncher.expectEvent(
+                            TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+                    mLauncher.expectEvent(
+                            TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+                }
+            }
+            mLauncher.waitForObjectInContainer(
+                    mWidgetCell.getParent().getParent().getParent().getParent(),
+                    By.text(ADD_AUTOMATICALLY)).click();
+            mLauncher.waitUntilLauncherObjectGone(getSelector());
+        }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 96e4b8c..b6c17df 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -18,6 +18,7 @@
 
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -29,6 +30,8 @@
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.testing.TestProtocol;
 
+import java.util.stream.Collectors;
+
 /**
  * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
@@ -41,16 +44,15 @@
     AllApps(LauncherInstrumentation launcher) {
         super(launcher);
         final UiObject2 allAppsContainer = verifyActiveContainer();
-        mHeight = allAppsContainer.getVisibleBounds().height();
+        mHeight = mLauncher.getVisibleBounds(allAppsContainer).height();
         final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
                 "apps_list_view");
         // Wait for the recycler to populate.
         mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class));
         verifyNotFrozen("All apps freeze flags upon opening all apps");
         mIconHeight = mLauncher.getTestInfo(
-                TestProtocol.REQUEST_ICON_HEIGHT)
-                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
-
+                TestProtocol.REQUEST_ICON_HEIGHT).
+                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
     @Override
@@ -58,14 +60,14 @@
         return LauncherInstrumentation.ContainerType.ALL_APPS;
     }
 
-    private boolean hasClickableIcon(
-            UiObject2 allAppsContainer, UiObject2 appListRecycler, BySelector appIconSelector) {
+    private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler,
+            BySelector appIconSelector, int displayBottom) {
         final UiObject2 icon = appListRecycler.findObject(appIconSelector);
         if (icon == null) {
             LauncherInstrumentation.log("hasClickableIcon: icon not visible");
             return false;
         }
-        final Rect iconBounds = icon.getVisibleBounds();
+        final Rect iconBounds = mLauncher.getVisibleBounds(icon);
         LauncherInstrumentation.log("hasClickableIcon: icon bounds: " + iconBounds);
         if (iconBounds.height() < mIconHeight / 2) {
             LauncherInstrumentation.log("hasClickableIcon: icon has insufficient height");
@@ -75,13 +77,17 @@
             LauncherInstrumentation.log("hasClickableIcon: icon center is under search box");
             return false;
         }
+        if (iconBounds.bottom > displayBottom) {
+            LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset");
+            return false;
+        }
         LauncherInstrumentation.log("hasClickableIcon: icon is clickable");
         return true;
     }
 
     private boolean iconCenterInSearchBox(UiObject2 allAppsContainer, UiObject2 icon) {
         final Point iconCenter = icon.getVisibleCenter();
-        return getSearchBox(allAppsContainer).getVisibleBounds().contains(
+        return mLauncher.getVisibleBounds(getSearchBox(allAppsContainer)).contains(
                 iconCenter.x, iconCenter.y);
     }
 
@@ -94,31 +100,41 @@
      */
     @NonNull
     public AppIcon getAppIcon(String appName) {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "getting app icon " + appName + " on all apps")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "getting app icon " + appName + " on all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
                     "apps_list_view");
             final UiObject2 searchBox = getSearchBox(allAppsContainer);
-            allAppsContainer.setGestureMargins(
-                    0,
-                    getSearchBox(allAppsContainer).getVisibleBounds().bottom + 1,
-                    0,
-                    ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
-                            mLauncher.getResources()) + 1);
+
+            int bottomGestureMargin = ResourceUtils.getNavbarSize(
+                    ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
+            int deviceHeight = mLauncher.getDevice().getDisplayHeight();
+            int displayBottom = deviceHeight - bottomGestureMargin;
             final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher);
-            if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector)) {
+            if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
+                    displayBottom)) {
                 scrollBackToBeginning();
                 int attempts = 0;
-                int scroll = getScroll(allAppsContainer);
+                int scroll = getAllAppsScroll();
                 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) {
-                    while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector)) {
+                    while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
+                            displayBottom)) {
                         mLauncher.scrollToLastVisibleRow(
                                 allAppsContainer,
-                                mLauncher.getObjectsInContainer(allAppsContainer, "icon"),
-                                searchBox.getVisibleBounds().bottom -
-                                        allAppsContainer.getVisibleBounds().top);
-                        final int newScroll = getScroll(allAppsContainer);
+                                mLauncher.getObjectsInContainer(allAppsContainer, "icon")
+                                        .stream()
+                                        .filter(icon ->
+                                                        mLauncher.getVisibleBounds(icon).bottom
+                                                        <= displayBottom)
+                                        .collect(Collectors.toList()),
+                                mLauncher.getVisibleBounds(searchBox).bottom
+                                        - mLauncher.getVisibleBounds(allAppsContainer).top);
+                        final int newScroll = getAllAppsScroll();
+                        mLauncher.assertTrue(
+                                "Scrolled in a wrong direction in AllApps: from " + scroll + " to "
+                                        + newScroll, newScroll >= scroll);
                         if (newScroll == scroll) break;
 
                         mLauncher.assertTrue(
@@ -131,8 +147,11 @@
                 verifyActiveContainer();
             }
 
+            // Ignore bottom offset selection here as there might not be any scroll more scroll
+            // region available.
             mLauncher.assertTrue("Unable to scroll to a clickable icon: " + appName,
-                    hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector));
+                    hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
+                            deviceHeight));
 
             final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler,
                     appIconSelector);
@@ -148,18 +167,20 @@
             final UiObject2 searchBox = getSearchBox(allAppsContainer);
 
             int attempts = 0;
-            final Rect margins = new Rect(0, searchBox.getVisibleBounds().bottom + 1, 0, 5);
+            final Rect margins =
+                    new Rect(0, mLauncher.getVisibleBounds(searchBox).bottom + 1, 0, 5);
 
-            for (int scroll = getScroll(allAppsContainer);
+            for (int scroll = getAllAppsScroll();
                     scroll != 0;
-                    scroll = getScroll(allAppsContainer)) {
+                    scroll = getAllAppsScroll()) {
                 mLauncher.assertTrue("Negative scroll position", scroll > 0);
 
                 mLauncher.assertTrue(
                         "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
                         ++attempts <= MAX_SCROLL_ATTEMPTS);
 
-                mLauncher.scroll(allAppsContainer, Direction.UP, margins, 50);
+                mLauncher.scroll(
+                        allAppsContainer, Direction.UP, margins, 12, false);
             }
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled up")) {
@@ -168,9 +189,10 @@
         }
     }
 
-    private int getScroll(UiObject2 allAppsContainer) {
-        return mLauncher.getAnswerFromLauncher(allAppsContainer, TestProtocol.GET_SCROLL_MESSAGE).
-                getInt(TestProtocol.SCROLL_Y_FIELD, -1);
+    private int getAllAppsScroll() {
+        return mLauncher.getTestInfo(
+                TestProtocol.REQUEST_APPS_LIST_SCROLL_Y)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
     private UiObject2 getSearchBox(UiObject2 allAppsContainer) {
@@ -181,12 +203,13 @@
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Start the gesture in the center to avoid starting at elements near the top.
             mLauncher.scroll(
-                    allAppsContainer, Direction.DOWN, new Rect(0, 0, 0, mHeight / 2), 10);
+                    allAppsContainer, Direction.DOWN, new Rect(0, 0, 0, mHeight / 2), 10, false);
             verifyActiveContainer();
         }
     }
@@ -195,12 +218,13 @@
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Start the gesture in the center, for symmetry with forward.
             mLauncher.scroll(
-                    allAppsContainer, Direction.UP, new Rect(0, mHeight / 2, 0, 0), 10);
+                    allAppsContainer, Direction.UP, new Rect(0, mHeight / 2, 0, 0), 10, false);
             verifyActiveContainer();
         }
     }
@@ -221,8 +245,8 @@
     }
 
     private void verifyNotFrozen(String message) {
-        mLauncher.assertEquals(message, 0, mLauncher.getTestInfo(
-                TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS).
-                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
+        final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS);
+        if (testInfo == null) return;
+        mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index f48d4dd..835790d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -42,8 +42,9 @@
      */
     @NonNull
     public Overview switchBackToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch back from all apps to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch back from all apps to overview")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
             // Swipe from the search box to the bottom.
             final UiObject2 qsb = mLauncher.waitForObjectInContainer(
@@ -55,7 +56,8 @@
 
             final int endY = start.y + swipeHeight;
             LauncherInstrumentation.log("AllAppsFromOverview.switchBackToOverview before swipe");
-            mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL);
+            mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL,
+                    LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("swiped down")) {
                 return new Overview(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..5de5b4a 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,19 +16,23 @@
 
 package com.android.launcher3.tapl;
 
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.view.MotionEvent;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.regex.Pattern;
+
 /**
  * App icon, whether in all apps or in workspace/
  */
 public final class AppIcon extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -41,18 +45,24 @@
      * Long-clicks the icon to open its menu.
      */
     public AppIconMenu openMenu() {
-        final Point iconCenter = mObject.getVisibleCenter();
-        final long downTime = SystemClock.uptimeMillis();
-        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
-        final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
-                "deep_shortcuts_container");
-        mLauncher.sendPointer(
-                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
-        return new AppIconMenu(mLauncher, deepShortcutsContainer);
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+                    mObject, "deep_shortcuts_container", LONG_CLICK_EVENT));
+        }
+    }
+
+    @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT);
     }
 
     @Override
     protected String getLongPressIndicator() {
         return "deep_shortcuts_container";
     }
+
+    @Override
+    protected void expectActivityStartEvents() {
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index ba9c10e..a40919b 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -18,10 +18,15 @@
 
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.regex.Pattern;
+
 /**
  * Menu item in an app icon menu.
  */
 public class AppIconMenuItem extends Launchable {
+
     AppIconMenuItem(LauncherInstrumentation launcher, UiObject2 shortcut) {
         super(launcher, shortcut);
     }
@@ -34,7 +39,16 @@
     }
 
     @Override
+    protected void addExpectedEventsForLongClick() {
+    }
+
+    @Override
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
+
+    @Override
+    protected void expectActivityStartEvents() {
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index db3d846..ce94a3e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,7 +16,9 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
 import android.graphics.Point;
 import android.os.SystemClock;
@@ -27,12 +29,15 @@
 
 import com.android.launcher3.testing.TestProtocol;
 
+import java.util.regex.Pattern;
+
 /**
  * Indicates the base state with a UI other than Overview running as foreground. It can also
  * indicate Launcher as long as Launcher is not in Overview state.
  */
 public class Background extends LauncherInstrumentation.VisibleContainer {
     private static final int ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION = 500;
+    private static final Pattern SQUARE_BUTTON_EVENT = Pattern.compile("onOverviewToggle");
 
     Background(LauncherInstrumentation launcher) {
         super(launcher);
@@ -51,16 +56,21 @@
      */
     @NonNull
     public BaseOverview switchToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from background to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from background to overview")) {
             verifyActiveContainer();
-            goToOverviewUnchecked(BACKGROUND_APP_STATE_ORDINAL);
+            goToOverviewUnchecked();
             return mLauncher.isFallbackOverview() ?
                     new BaseOverview(mLauncher) : new Overview(mLauncher);
         }
     }
 
-    protected void goToOverviewUnchecked(int expectedState) {
+    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+        return false;
+    }
+
+    protected void goToOverviewUnchecked() {
         switch (mLauncher.getNavigationModel()) {
             case ZERO_BUTTON: {
                 final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
@@ -72,18 +82,32 @@
                         new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
 
                 final long downTime = SystemClock.uptimeMillis();
-                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
+                final LauncherInstrumentation.GestureScope gestureScope =
+                        zeroButtonToOverviewGestureStartsInLauncher()
+                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                : LauncherInstrumentation.GestureScope.OUTSIDE;
+
+                // b/156044202
+                mLauncher.log("Hierarchy before swiping up to overview:");
+                mLauncher.dumpViewHierarchy();
+
+                mLauncher.sendPointer(
+                        downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
                 mLauncher.executeAndWaitForEvent(
                         () -> mLauncher.movePointer(
                                 downTime,
                                 downTime,
                                 ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
                                 start,
-                                end),
+                                end,
+                                gestureScope),
                         event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(event.getClassName()),
-                        "Pause wasn't detected");
-                mLauncher.sendPointer(
-                        downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end);
+                        () -> "Pause wasn't detected");
+                mLauncher.runToState(
+                        () -> mLauncher.sendPointer(
+                                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
+                                gestureScope),
+                        OVERVIEW_STATE_ORDINAL);
                 break;
             }
 
@@ -105,22 +129,31 @@
                     startY = endY = mLauncher.getDevice().getDisplayHeight() / 2;
                 }
 
-                mLauncher.swipeToState(startX, startY, endX, endY, 10, expectedState);
+                mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
+                        LauncherInstrumentation.GestureScope.OUTSIDE);
                 break;
             }
 
             case THREE_BUTTON:
-                mLauncher.waitForSystemUiObject("recent_apps").click();
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                mLauncher.runToState(
+                        () -> mLauncher.waitForSystemUiObject("recent_apps").click(),
+                        OVERVIEW_STATE_ORDINAL);
                 break;
         }
+        expectSwitchToOverviewEvents();
+    }
+
+    private void expectSwitchToOverviewEvents() {
     }
 
     /**
      * Swipes right or double presses the square button to switch to the previous app.
      */
     public Background quickSwitchToPreviousApp() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to quick switch to the previous app")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to quick switch to the previous app")) {
             verifyActiveContainer();
             quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
             return new Background(mLauncher);
@@ -132,6 +165,7 @@
     }
 
     protected void quickSwitchToPreviousApp(int expectedState) {
+        final boolean launcherWasVisible = mLauncher.isLauncherVisible();
         boolean transposeInLandscape = false;
         switch (mLauncher.getNavigationModel()) {
             case TWO_BUTTON:
@@ -155,18 +189,26 @@
                     endX = startX;
                     endY = 0;
                 }
-                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState);
+                final boolean isZeroButton = mLauncher.getNavigationModel()
+                        == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
+                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
+                        launcherWasVisible && isZeroButton
+                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                : LauncherInstrumentation.GestureScope.OUTSIDE);
                 break;
             }
 
             case THREE_BUTTON:
                 // Double press the recents button.
                 UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
-                recentsButton.click();
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
                 mLauncher.getOverview();
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
                 recentsButton.click();
                 break;
         }
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
     }
 
     protected String getSwipeHeightRequestName() {
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 49c0c89..223ae29 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -23,8 +23,6 @@
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
-import com.android.launcher3.testing.TestProtocol;
-
 import java.util.Collections;
 import java.util.List;
 
@@ -48,14 +46,19 @@
      * Flings forward (left) and waits the fling's end.
      */
     public void flingForward() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            flingForwardImpl();
+        }
+    }
+
+    private void flingForwardImpl() {
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling forward in overview")) {
             LauncherInstrumentation.log("Overview.flingForward before fling");
             final UiObject2 overview = verifyActiveContainer();
-            final int leftMargin = mLauncher.getTestInfo(
-                    TestProtocol.REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN).
-                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
-            mLauncher.scroll(overview, Direction.LEFT, new Rect(leftMargin, 0, 0, 0), 20);
+            final int leftMargin = mLauncher.getTargetInsets().left;
+            mLauncher.scroll(
+                    overview, Direction.LEFT, new Rect(leftMargin + 1, 0, 0, 0), 20, false);
             verifyActiveContainer();
         }
     }
@@ -63,22 +66,20 @@
     /**
      * Dismissed all tasks by scrolling to Clear-all button and pressing it.
      */
-    public Workspace dismissAllTasks() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "dismissing all tasks")) {
-            final BySelector clearAllSelector = mLauncher.getLauncherObjectSelector("clear_all");
+    public void dismissAllTasks() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "dismissing all tasks")) {
+            final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
             for (int i = 0;
                     i < FLINGS_FOR_DISMISS_LIMIT
                             && !verifyActiveContainer().hasObject(clearAllSelector);
                     ++i) {
-                flingForward();
+                flingForwardImpl();
             }
 
-            mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector).click();
-            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                    "dismissed all tasks")) {
-                return new Workspace(mLauncher);
-            }
+            mLauncher.clickLauncherObject(
+                    mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector));
         }
     }
 
@@ -86,14 +87,14 @@
      * Flings backward (right) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to fling backward in overview")) {
             LauncherInstrumentation.log("Overview.flingBackward before fling");
             final UiObject2 overview = verifyActiveContainer();
-            final int rightMargin = mLauncher.getTestInfo(
-                    TestProtocol.REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN).
-                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
-            mLauncher.scroll(overview, Direction.RIGHT, new Rect(0, 0, rightMargin, 0), 20);
+            final int rightMargin = mLauncher.getTargetInsets().right;
+            mLauncher.scroll(
+                    overview, Direction.RIGHT, new Rect(0, 0, rightMargin + 1, 0), 20, false);
             verifyActiveContainer();
         }
     }
@@ -105,21 +106,33 @@
      */
     @NonNull
     public OverviewTask getCurrentTask() {
+        final List<UiObject2> taskViews = getTasks();
+        mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
+
+        // taskViews contains up to 3 task views: the 'main' (having the widest visible part) one
+        // in the center, and parts of its right and left siblings. Find the main task view by
+        // its width.
+        final UiObject2 widestTask = Collections.max(taskViews,
+                (t1, t2) -> Integer.compare(mLauncher.getVisibleBounds(t1).width(),
+                        mLauncher.getVisibleBounds(t2).width()));
+
+        return new OverviewTask(mLauncher, widestTask, this);
+    }
+
+    @NonNull
+    private List<UiObject2> getTasks() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to get current task")) {
+                "want to get overview tasks")) {
             verifyActiveContainer();
-            final List<UiObject2> taskViews = mLauncher.getDevice().findObjects(
-                    mLauncher.getLauncherObjectSelector("snapshot"));
-            mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
-
-            // taskViews contains up to 3 task views: the 'main' (having the widest visible
-            // part) one in the center, and parts of its right and left siblings. Find the
-            // main task view by its width.
-            final UiObject2 widestTask = Collections.max(taskViews,
-                    (t1, t2) -> Integer.compare(t1.getVisibleBounds().width(),
-                            t2.getVisibleBounds().width()));
-
-            return new OverviewTask(mLauncher, widestTask, this);
+            return mLauncher.getDevice().findObjects(
+                    mLauncher.getOverviewObjectSelector("snapshot"));
         }
     }
+
+    /**
+     * Returns whether Overview has tasks.
+     */
+    public boolean hasTasks() {
+        return getTasks().size() > 0;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java
deleted file mode 100644
index 6e6734d..0000000
--- a/tests/tapl/com/android/launcher3/tapl/Folder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.tapl;
-
-import android.widget.FrameLayout;
-
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiObject2;
-
-/**
- * App folder in workspace/
- */
-public final class Folder {
-    Folder(LauncherInstrumentation launcher, UiObject2 icon) {
-    }
-
-    static BySelector getSelector(String folderName, LauncherInstrumentation launcher) {
-        return By.clazz(FrameLayout.class).desc(folderName).pkg(launcher.getLauncherPackageName());
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index e0fe933..c06e254 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
 
 import androidx.annotation.NonNull;
@@ -49,10 +48,11 @@
     @NonNull
     @Override
     public Overview switchToOverview() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from home to overview")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from home to overview")) {
             verifyActiveContainer();
-            goToOverviewUnchecked(OVERVIEW_STATE_ORDINAL);
+            goToOverviewUnchecked();
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "performed the switch action")) {
                 return new Overview(mLauncher);
@@ -61,6 +61,11 @@
     }
 
     @Override
+    protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+        return true;
+    }
+
+    @Override
     protected int getExpectedStateForQuickSwitch() {
         return QUICK_SWITCH_STATE_ORDINAL;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index df80a51..13ecfb8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -46,17 +46,22 @@
      * Clicks the object to launch its app.
      */
     public Background launch(String expectedPackageName) {
-        return launch(By.pkg(expectedPackageName));
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            return launch(By.pkg(expectedPackageName));
+        }
     }
 
+    protected abstract void expectActivityStartEvents();
+
     private Background launch(BySelector selector) {
         LauncherInstrumentation.log("Launchable.launch before click " +
-                mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+                mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
 
         mLauncher.executeAndWaitForEvent(
-                () -> mObject.click(),
+                () -> mLauncher.clickLauncherObject(mObject),
                 event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                "Launching an app didn't open a new window: " + mObject.getText());
+                () -> "Launching an app didn't open a new window: " + mObject.getText());
+        expectActivityStartEvents();
 
         mLauncher.assertTrue(
                 "App didn't start: " + selector,
@@ -67,20 +72,31 @@
 
     /**
      * Drags an object to the center of homescreen.
+     *
+     * @param startsActivity   whether it's expected to start an activity.
+     * @param isWidgetShortcut whether we drag a widget shortcut
      */
-    public void dragToWorkspace() {
-        final Point launchableCenter = getObject().getVisibleCenter();
-        final Point displaySize = mLauncher.getRealDisplaySize();
-        final int width = displaySize.x / 2;
-        Workspace.dragIconToWorkspace(
-                mLauncher,
-                this,
-                new Point(
-                        launchableCenter.x >= width ?
-                                launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
-                        displaySize.y / 2),
-                getLongPressIndicator());
+    public void dragToWorkspace(boolean startsActivity, boolean isWidgetShortcut) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final Point launchableCenter = getObject().getVisibleCenter();
+            final Point displaySize = mLauncher.getRealDisplaySize();
+            final int width = displaySize.x / 2;
+            Workspace.dragIconToWorkspace(
+                    mLauncher,
+                    this,
+                    new Point(
+                            launchableCenter.x >= width
+                                    ? launchableCenter.x - width / 2
+                                    : launchableCenter.x + width / 2,
+                            displaySize.y / 2),
+                    getLongPressIndicator(),
+                    startsActivity,
+                    isWidgetShortcut,
+                    () -> addExpectedEventsForLongClick());
+        }
     }
 
+    protected abstract void addExpectedEventsForLongClick();
+
     protected abstract String getLongPressIndicator();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index c52650d..f4274a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -22,23 +22,25 @@
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
 
 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
-import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.ComponentName;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
+import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Log;
@@ -50,12 +52,12 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Configurator;
 import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.StaleObjectException;
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
@@ -69,6 +71,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
@@ -77,6 +80,9 @@
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 /**
  * The main tapl object. The only object that can be explicitly constructed by the using code. It
@@ -89,6 +95,15 @@
     private static final int GESTURE_STEP_MS = 16;
     private static long START_TIME = System.currentTimeMillis();
 
+    private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
+    private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
+    private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
+    private static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
+    static final Pattern EVENT_START = Pattern.compile("start:");
+
+    static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
+    static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
+
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
     public enum ContainerType {
@@ -97,6 +112,13 @@
 
     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
 
+    // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+    public enum GestureScope {
+        OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+    }
+
+    ;
+
     // Base class for launcher containers.
     static abstract class VisibleContainer {
         protected final LauncherInstrumentation mLauncher;
@@ -120,7 +142,7 @@
         }
     }
 
-    interface Closable extends AutoCloseable {
+    public interface Closable extends AutoCloseable {
         void close();
     }
 
@@ -128,6 +150,7 @@
     private static final String APPS_RES_ID = "apps_view";
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "widgets_list_view";
+    private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
 
@@ -142,6 +165,27 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
+    private LogEventChecker mEventChecker;
+
+    private boolean mCheckEventsForSuccessfulGestures = false;
+    private Runnable mOnLauncherCrashed;
+
+    private static Pattern getTouchEventPattern(String prefix, String action) {
+        // The pattern includes sanity checks that we don't get a multi-touch events or other
+        // surprises.
+        return Pattern.compile(
+                prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+                        + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
+    }
+
+    private static Pattern getTouchEventPattern(String action) {
+        return getTouchEventPattern("Touch event", action);
+    }
+
+    private static Pattern getTouchEventPatternTIS(String action) {
+        return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
+    }
+
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
      */
@@ -180,13 +224,8 @@
                 .authority(testProviderAuthority)
                 .build();
 
-        try {
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.WRITE_SECURE_SETTINGS");
-        } catch (IOException e) {
-            fail(e.toString());
-        }
-
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.WRITE_SECURE_SETTINGS");
 
         PackageManager pm = getContext().getPackageManager();
         ProviderInfo pi = pm.resolveContentProvider(
@@ -208,12 +247,30 @@
         }
     }
 
+    public void enableCheckEventsForSuccessfulGestures() {
+        mCheckEventsForSuccessfulGestures = true;
+    }
+
+    public void setOnLauncherCrashed(Runnable onLauncherCrashed) {
+        mOnLauncherCrashed = onLauncherCrashed;
+    }
+
     Context getContext() {
         return mInstrumentation.getContext();
     }
 
     Bundle getTestInfo(String request) {
-        return getContext().getContentResolver().call(mTestProviderUri, request, null, null);
+        try (ContentProviderClient client = getContext().getContentResolver()
+                .acquireContentProviderClient(mTestProviderUri)) {
+            return client.call(request, null, null);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    Insets getTargetInsets() {
+        return getTestInfo(TestProtocol.REQUEST_WINDOW_INSETS)
+                .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
     void setActiveContainer(VisibleContainer container) {
@@ -257,14 +314,14 @@
 
     Closable addContextLayer(String piece) {
         mDiagnosticContext.addLast(piece);
-        log("Added context: " + getContextDescription());
+        log("Entering context: " + piece);
         return () -> {
-            log("Removing context: " + getContextDescription());
+            log("Leaving context: " + piece);
             mDiagnosticContext.removeLast();
         };
     }
 
-    private void dumpViewHierarchy() {
+    public void dumpViewHierarchy() {
         final ByteArrayOutputStream stream = new ByteArrayOutputStream();
         try {
             mDevice.dumpWindowHierarchy(stream);
@@ -278,31 +335,74 @@
         }
     }
 
-    private String getAnomalyMessage() {
-        UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
-        if (object != null) {
-            return "System alert popup is visible: " + object.getText();
+    private String getSystemAnomalyMessage() {
+        try {
+            {
+                final StringBuilder sb = new StringBuilder();
+
+                UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
+                if (object != null) {
+                    sb.append("TITLE: ").append(object.getText());
+                }
+
+                object = mDevice.findObject(By.res("android", "message"));
+                if (object != null) {
+                    sb.append(" PACKAGE: ").append(object.getApplicationPackage())
+                            .append(" MESSAGE: ").append(object.getText());
+                }
+
+                if (sb.length() != 0) {
+                    return "System alert popup is visible: " + sb;
+                }
+            }
+
+            if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
+
+            if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+
+            final String navigationModeError = getNavigationModeMismatchError();
+            if (navigationModeError != null) return navigationModeError;
+        } catch (Throwable e) {
+            Log.w(TAG, "getSystemAnomalyMessage failed", e);
         }
 
-        object = mDevice.findObject(By.res("android", "message"));
-        if (object != null) {
-            return "Message popup by " + object.getApplicationPackage() + " is visible: "
-                    + object.getText();
-        }
-
-        if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
-
-        if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
-
         return null;
     }
 
+    public void checkForAnomaly() {
+        final String systemAnomalyMessage = getSystemAnomalyMessage();
+        if (systemAnomalyMessage != null) {
+            Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
+                    "http://go/tapl : Tests are broken by a non-Launcher system error: "
+                            + systemAnomalyMessage, false)));
+        }
+    }
+
+    private String getVisiblePackages() {
+        return mDevice.findObjects(By.textStartsWith(""))
+                .stream()
+                .map(LauncherInstrumentation::getApplicationPackageSafe)
+                .distinct()
+                .filter(pkg -> pkg != null && !"com.android.systemui".equals(pkg))
+                .collect(Collectors.joining(", "));
+    }
+
+    private static String getApplicationPackageSafe(UiObject2 object) {
+        try {
+            return object.getApplicationPackage();
+        } catch (StaleObjectException e) {
+            // We are looking at all object in the system; external ones can suddenly go away.
+            return null;
+        }
+    }
+
     private String getVisibleStateMessage() {
+        if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
-        return "Background";
+        return "Background (" + getVisiblePackages() + ")";
     }
 
     public void setSystemHealthSupplier(Function<Long, String> supplier) {
@@ -313,44 +413,72 @@
         mOnSettledStateAction = onSettledStateAction;
     }
 
-    private String getSystemHealthMessage() {
+    private String formatSystemHealthMessage(String message) {
         final String testPackage = getContext().getPackageName();
-        try {
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.READ_LOGS");
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.PACKAGE_USAGE_STATS");
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
 
-        return mSystemHealthSupplier != null
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.READ_LOGS");
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.PACKAGE_USAGE_STATS");
+
+        final String systemHealth = mSystemHealthSupplier != null
                 ? mSystemHealthSupplier.apply(START_TIME)
                 : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
-    }
 
-    private void fail(String message) {
-        message = "http://go/tapl : " + getContextDescription() + message;
-
-        final String anomaly = getAnomalyMessage();
-        if (anomaly != null) {
-            message = anomaly + ", which causes:\n" + message;
-        } else {
-            message = message + " (visible state: " + getVisibleStateMessage() + ")";
-        }
-
-        final String systemHealth = getSystemHealthMessage();
         if (systemHealth != null) {
-            message = message
-                    + ", which might be a consequence of system health "
-                    + "problems:\n<<<<<<<<<<<<<<<<<<\n"
+            return message
+                    + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
                     + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
         }
 
+        return message;
+    }
+
+    private String formatErrorWithEvents(String message, boolean checkEvents) {
+        if (mEventChecker != null) {
+            final LogEventChecker eventChecker = mEventChecker;
+            mEventChecker = null;
+            if (checkEvents) {
+                final String eventMismatch = eventChecker.verify(0, false);
+                if (eventMismatch != null) {
+                    message = message + ", having produced " + eventMismatch;
+                }
+            } else {
+                eventChecker.finishNoWait();
+            }
+        }
+
+        dumpDiagnostics();
+
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        Assert.fail(message);
+        return message;
+    }
+
+    private void dumpDiagnostics() {
+        Log.e("b/156287114", "Input:");
+        logShellCommand("dumpsys input");
+        Log.e("b/156287114", "TIS:");
+        logShellCommand("dumpsys activity service TouchInteractionService");
+    }
+
+    private void logShellCommand(String command) {
+        try {
+            for (String line : mDevice.executeShellCommand(command).split("\\n")) {
+                SystemClock.sleep(10);
+                Log.d("b/156287114", line);
+            }
+        } catch (IOException e) {
+            Log.d("b/156287114", "Failed to execute " + command);
+        }
+    }
+
+    private void fail(String message) {
+        checkForAnomaly();
+        Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
+                "http://go/tapl : " + getContextDescription() + message
+                        + " (visible state: " + getVisibleStateMessage() + ")", true)));
     }
 
     private String getContextDescription() {
@@ -420,18 +548,15 @@
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
 
-        // b/136278866
-        for (int i = 0; i != 100; ++i) {
-            if (getNavigationModeMismatchError() == null) break;
-            try {
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
+        // b/148422894
+        String error = null;
+        for (int i = 0; i != 600; ++i) {
+            error = getNavigationModeMismatchError();
+            if (error == null) break;
+            sleep(100);
         }
-
-        final String error = getNavigationModeMismatchError();
         assertTrue(error, error == null);
+
         log("verifyContainerType: " + containerType);
 
         final UiObject2 container = verifyVisibleObjects(containerType);
@@ -449,32 +574,32 @@
                     if (mDevice.isNaturalOrientation()) {
                         waitForLauncherObject(APPS_RES_ID);
                     } else {
-                        waitUntilGone(APPS_RES_ID);
+                        waitUntilLauncherObjectGone(APPS_RES_ID);
                     }
-                    waitUntilGone(OVERVIEW_RES_ID);
-                    waitUntilGone(WIDGETS_RES_ID);
+                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     return waitForLauncherObject(WORKSPACE_RES_ID);
                 }
                 case WIDGETS: {
-                    waitUntilGone(WORKSPACE_RES_ID);
-                    waitUntilGone(APPS_RES_ID);
-                    waitUntilGone(OVERVIEW_RES_ID);
+                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+                    waitUntilLauncherObjectGone(APPS_RES_ID);
+                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
                     return waitForLauncherObject(WIDGETS_RES_ID);
                 }
                 case ALL_APPS: {
-                    waitUntilGone(WORKSPACE_RES_ID);
-                    waitUntilGone(OVERVIEW_RES_ID);
-                    waitUntilGone(WIDGETS_RES_ID);
+                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     return waitForLauncherObject(APPS_RES_ID);
                 }
                 case OVERVIEW: {
-                    if (mDevice.isNaturalOrientation()) {
+                    if (hasAllAppsInOverview()) {
                         waitForLauncherObject(APPS_RES_ID);
                     } else {
-                        waitUntilGone(APPS_RES_ID);
+                        waitUntilLauncherObjectGone(APPS_RES_ID);
                     }
-                    waitUntilGone(WORKSPACE_RES_ID);
-                    waitUntilGone(WIDGETS_RES_ID);
+                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
 
                     return waitForLauncherObject(OVERVIEW_RES_ID);
                 }
@@ -482,10 +607,10 @@
                     return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
                 }
                 case BACKGROUND: {
-                    waitUntilGone(WORKSPACE_RES_ID);
-                    waitUntilGone(APPS_RES_ID);
-                    waitUntilGone(OVERVIEW_RES_ID);
-                    waitUntilGone(WIDGETS_RES_ID);
+                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+                    waitUntilLauncherObjectGone(APPS_RES_ID);
+                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     return null;
                 }
                 default:
@@ -495,7 +620,7 @@
         }
     }
 
-    private void waitForLauncherInitialized() {
+    public void waitForLauncherInitialized() {
         for (int i = 0; i < 100; ++i) {
             if (getTestInfo(
                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
@@ -508,85 +633,97 @@
     }
 
     Parcelable executeAndWaitForEvent(Runnable command,
-            UiAutomation.AccessibilityEventFilter eventFilter, String message) {
+            UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
         try {
             final AccessibilityEvent event =
                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
                             command, eventFilter, WAIT_TIME_MS);
             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
-            return event.getParcelableData();
+            final Parcelable parcelableData = event.getParcelableData();
+            event.recycle();
+            return parcelableData;
         } catch (TimeoutException e) {
-            fail(message);
+            fail(message.get());
             return null;
         }
     }
 
-    Bundle getAnswerFromLauncher(UiObject2 view, String requestTag) {
-        // Send a fake set-text request to Launcher to initiate a response with requested data.
-        final String responseTag = requestTag + TestProtocol.RESPONSE_MESSAGE_POSTFIX;
-        return (Bundle) executeAndWaitForEvent(
-                () -> view.setText(requestTag),
-                event -> responseTag.equals(event.getClassName()),
-                "Launcher didn't respond to request: " + requestTag);
-    }
-
     /**
      * Presses nav bar home button.
      *
      * @return the Workspace object.
      */
     public Workspace pressHome() {
-        // Click home, then wait for any accessibility event, then wait until accessibility events
-        // stop.
-        // We need waiting for any accessibility event generated after pressing Home because
-        // otherwise waitForIdle may return immediately in case when there was a big enough pause in
-        // accessibility events prior to pressing Home.
-        final String action;
-        if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
-            final String anomaly = getAnomalyMessage();
-            if (anomaly != null) fail("Can't swipe up to Home: " + anomaly);
+        try (LauncherInstrumentation.Closable e = eventsCheck()) {
+            waitForLauncherInitialized();
+            // Click home, then wait for any accessibility event, then wait until accessibility
+            // events stop.
+            // We need waiting for any accessibility event generated after pressing Home because
+            // otherwise waitForIdle may return immediately in case when there was a big enough
+            // pause in accessibility events prior to pressing Home.
+            final String action;
+            final boolean launcherWasVisible = isLauncherVisible();
+            if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+                checkForAnomaly();
 
-            final Point displaySize = getRealDisplaySize();
+                final Point displaySize = getRealDisplaySize();
 
-            if (hasLauncherObject("deep_shortcuts_container")) {
-                linearGesture(
-                        displaySize.x / 2, displaySize.y - 1,
-                        displaySize.x / 2, 0,
-                        ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
-                try (LauncherInstrumentation.Closable c = addContextLayer(
-                        "Swiped up from context menu to home")) {
-                    waitUntilGone("deep_shortcuts_container");
+                if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
+                    linearGesture(
+                            displaySize.x / 2, displaySize.y - 1,
+                            displaySize.x / 2, 0,
+                            ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                            false, GestureScope.INSIDE_TO_OUTSIDE);
+                    try (LauncherInstrumentation.Closable c = addContextLayer(
+                            "Swiped up from context menu to home")) {
+                        waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID);
+                    }
+                }
+                if (hasLauncherObject(WORKSPACE_RES_ID)) {
+                    log(action = "already at home");
+                } else {
+                    log("Hierarchy before swiping up to home:");
+                    dumpViewHierarchy();
+                    action = "swiping up to home";
+
+                    try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+                        swipeToState(
+                                displaySize.x / 2, displaySize.y - 1,
+                                displaySize.x / 2, 0,
+                                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
+                                launcherWasVisible
+                                        ? GestureScope.INSIDE_TO_OUTSIDE
+                                        : GestureScope.OUTSIDE);
+                    }
+                }
+            } else {
+                log("Hierarchy before clicking home:");
+                dumpViewHierarchy();
+                action = "clicking home button";
+                try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+                    if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
+                        expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+                        expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
+                    }
+
+                    runToState(
+                            waitForSystemUiObject("home")::click,
+                            NORMAL_STATE_ORDINAL,
+                            !hasLauncherObject(WORKSPACE_RES_ID)
+                                    && (hasLauncherObject(APPS_RES_ID)
+                                    || hasLauncherObject(OVERVIEW_RES_ID)));
                 }
             }
-            if (hasLauncherObject(WORKSPACE_RES_ID)) {
-                log(action = "already at home");
-            } else {
-                log("Hierarchy before swiping up to home");
-                dumpViewHierarchy();
-                log(action = "swiping up to home from " + getVisibleStateMessage());
-                final int finalState = mDevice.hasObject(By.pkg(getLauncherPackageName()))
-                        ? NORMAL_STATE_ORDINAL : BACKGROUND_APP_STATE_ORDINAL;
-
-                swipeToState(
-                        displaySize.x / 2, displaySize.y - 1,
-                        displaySize.x / 2, 0,
-                        ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, finalState);
+            try (LauncherInstrumentation.Closable c = addContextLayer(
+                    "performed action to switch to Home - " + action)) {
+                return getWorkspace();
             }
-        } else {
-            log(action = "clicking home button");
-            executeAndWaitForEvent(
-                    () -> {
-                        log("LauncherInstrumentation.pressHome before clicking");
-                        waitForSystemUiObject("home").click();
-                    },
-                    event -> true,
-                    "Pressing Home didn't produce any events");
-            mDevice.waitForIdle();
         }
-        try (LauncherInstrumentation.Closable c = addContextLayer(
-                "performed action to switch to Home - " + action)) {
-            return getWorkspace();
-        }
+    }
+
+    boolean isLauncherVisible() {
+        mDevice.waitForIdle();
+        return hasLauncherObject(By.textStartsWith(""));
     }
 
     /**
@@ -676,9 +813,31 @@
         }
     }
 
-    void waitUntilGone(String resId) {
-        assertTrue("Unexpected launcher object visible: " + resId,
-                mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
+    /**
+     * Gets the Options Popup Menu object if the current state is showing the popup menu. Fails if
+     * the launcher is not in that state.
+     *
+     * @return Options Popup Menu object.
+     */
+    @NonNull
+    public OptionsPopupMenu getOptionsPopupMenu() {
+        try (LauncherInstrumentation.Closable c = addContextLayer(
+                "want to get context menu object")) {
+            return new OptionsPopupMenu(this);
+        }
+    }
+
+    void waitUntilLauncherObjectGone(String resId) {
+        waitUntilGoneBySelector(getLauncherObjectSelector(resId));
+    }
+
+    void waitUntilLauncherObjectGone(BySelector selector) {
+        waitUntilGoneBySelector(makeLauncherSelector(selector));
+    }
+
+    private void waitUntilGoneBySelector(BySelector launcherSelector) {
+        assertTrue("Unexpected launcher object visible: " + launcherSelector,
+                mDevice.wait(Until.gone(launcherSelector),
                         WAIT_TIME_MS));
     }
 
@@ -696,34 +855,56 @@
 
     @NonNull
     List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
-        return container.findObjects(getLauncherObjectSelector(resName));
+        try {
+            return container.findObjects(getLauncherObjectSelector(resName));
+        } catch (StaleObjectException e) {
+            fail("The container disappeared from screen");
+            return null;
+        }
     }
 
     @NonNull
     UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
-        final UiObject2 object = container.wait(
-                Until.findObject(getLauncherObjectSelector(resName)),
-                WAIT_TIME_MS);
-        assertNotNull("Can't find a launcher object id: " + resName + " in container: " +
-                container.getResourceName(), object);
-        return object;
+        try {
+            final UiObject2 object = container.wait(
+                    Until.findObject(getLauncherObjectSelector(resName)),
+                    WAIT_TIME_MS);
+            assertNotNull("Can't find a view in Launcher, id: " + resName + " in container: "
+                    + container.getResourceName(), object);
+            return object;
+        } catch (StaleObjectException e) {
+            fail("The container disappeared from screen");
+            return null;
+        }
     }
 
     @NonNull
     UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
-        final UiObject2 object = container.wait(
-                Until.findObject(selector),
-                WAIT_TIME_MS);
-        assertNotNull("Can't find a launcher object id: " + selector + " in container: " +
-                container.getResourceName(), object);
-        return object;
+        try {
+            final UiObject2 object = container.wait(
+                    Until.findObject(selector),
+                    WAIT_TIME_MS);
+            assertNotNull("Can't find a view in Launcher, id: " + selector + " in container: "
+                    + container.getResourceName(), object);
+            return object;
+        } catch (StaleObjectException e) {
+            fail("The container disappeared from screen");
+            return null;
+        }
     }
 
-    @Nullable
     private boolean hasLauncherObject(String resId) {
         return mDevice.hasObject(getLauncherObjectSelector(resId));
     }
 
+    boolean hasLauncherObject(BySelector selector) {
+        return mDevice.hasObject(makeLauncherSelector(selector));
+    }
+
+    private BySelector makeLauncherSelector(BySelector selector) {
+        return By.copy(selector).pkg(getLauncherPackageName());
+    }
+
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
         return waitForObjectBySelector(getLauncherObjectSelector(resName));
@@ -731,22 +912,22 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(BySelector selector) {
-        return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
+        return waitForObjectBySelector(makeLauncherSelector(selector));
     }
 
     @NonNull
     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
-        return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
+        return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
     }
 
     @NonNull
     UiObject2 waitForFallbackLauncherObject(String resName) {
-        return waitForObjectBySelector(getFallbackLauncherObjectSelector(resName));
+        return waitForObjectBySelector(getOverviewObjectSelector(resName));
     }
 
     private UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
-        assertNotNull("Can't find a launcher object; selector: " + selector, object);
+        assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
         return object;
     }
 
@@ -758,7 +939,7 @@
         return By.res(getLauncherPackageName(), resName);
     }
 
-    BySelector getFallbackLauncherObjectSelector(String resName) {
+    BySelector getOverviewObjectSelector(String resName) {
         return By.res(getOverviewPackageName(), resName);
     }
 
@@ -775,14 +956,49 @@
         return mDevice;
     }
 
-    void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState) {
-        final Bundle parcel = (Bundle) executeAndWaitForEvent(
-                () -> linearGesture(startX, startY, endX, endY, steps),
-                event -> TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName()),
-                "Swipe failed to receive an event for the swipe end");
-        assertEquals("Swipe switched launcher to a wrong state;",
-                TestProtocol.stateOrdinalToString(expectedState),
-                TestProtocol.stateOrdinalToString(parcel.getInt(TestProtocol.STATE_FIELD)));
+    private static String eventListToString(List<Integer> actualEvents) {
+        if (actualEvents.isEmpty()) return "no events";
+
+        return "["
+                + actualEvents.stream()
+                .map(state -> TestProtocol.stateOrdinalToString(state))
+                .collect(Collectors.joining(", "))
+                + "]";
+    }
+
+    void runToState(Runnable command, int expectedState, boolean requireEvent) {
+        if (requireEvent) {
+            runToState(command, expectedState);
+        } else {
+            command.run();
+        }
+    }
+
+    void runToState(Runnable command, int expectedState) {
+        final List<Integer> actualEvents = new ArrayList<>();
+        executeAndWaitForEvent(
+                command,
+                event -> isSwitchToStateEvent(event, expectedState, actualEvents),
+                () -> "Failed to receive an event for the state change: expected ["
+                        + TestProtocol.stateOrdinalToString(expectedState)
+                        + "], actual: " + eventListToString(actualEvents));
+    }
+
+    private boolean isSwitchToStateEvent(
+            AccessibilityEvent event, int expectedState, List<Integer> actualEvents) {
+        if (!TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName())) return false;
+
+        final Bundle parcel = (Bundle) event.getParcelableData();
+        final int actualState = parcel.getInt(TestProtocol.STATE_FIELD);
+        actualEvents.add(actualState);
+        return actualState == expectedState;
+    }
+
+    void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
+            GestureScope gestureScope) {
+        runToState(
+                () -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
+                expectedState);
     }
 
     int getBottomGestureSize() {
@@ -790,32 +1006,49 @@
                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
     }
 
-    int getBottomGestureMargin(UiObject2 container) {
-        return container.getVisibleBounds().bottom - getRealDisplaySize().y +
-                getBottomGestureSize();
+    int getBottomGestureMarginInContainer(UiObject2 container) {
+        final int bottomGestureStartOnScreen = getRealDisplaySize().y - getBottomGestureSize();
+        return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
     }
 
-    void scrollToLastVisibleRow(UiObject2 container, Collection<UiObject2> items, int topPadding) {
+    void clickLauncherObject(UiObject2 object) {
+        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
+        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
+        if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+            expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+            expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+        }
+        object.click();
+    }
+
+    void scrollToLastVisibleRow(
+            UiObject2 container,
+            Collection<UiObject2> items,
+            int topPaddingInContainer) {
         final UiObject2 lowestItem = Collections.max(items, (i1, i2) ->
-                Integer.compare(i1.getVisibleBounds().top, i2.getVisibleBounds().top));
+                Integer.compare(getVisibleBounds(i1).top, getVisibleBounds(i2).top));
 
-        final int gestureStart = lowestItem.getVisibleBounds().top + getTouchSlop();
-        final int distance = gestureStart - container.getVisibleBounds().top - topPadding;
-        final int bottomMargin = container.getVisibleBounds().height() - distance;
+        final int itemRowCurrentTopOnScreen = getVisibleBounds(lowestItem).top;
+        final Rect containerRect = getVisibleBounds(container);
+        final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
+        final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
 
+        final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
         scroll(
                 container,
                 Direction.DOWN,
                 new Rect(
                         0,
+                        containerRect.height() - distance - bottomGestureMarginInContainer,
                         0,
-                        0,
-                        Math.max(bottomMargin, getBottomGestureMargin(container))),
-                150);
+                        bottomGestureMarginInContainer),
+                10,
+                true);
     }
 
-    void scroll(UiObject2 container, Direction direction, Rect margins, int steps) {
-        final Rect rect = container.getVisibleBounds();
+    void scroll(
+            UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown) {
+        final Rect rect = getVisibleBounds(container);
         if (margins != null) {
             rect.left += margins.left;
             rect.top += margins.top;
@@ -831,34 +1064,26 @@
         switch (direction) {
             case UP: {
                 startX = endX = rect.centerX();
-                final int vertCenter = rect.centerY();
-                final float halfGestureHeight = rect.height() / 2.0f;
-                startY = (int) (vertCenter - halfGestureHeight) + 1;
-                endY = (int) (vertCenter + halfGestureHeight);
+                startY = rect.top;
+                endY = rect.bottom - 1;
             }
             break;
             case DOWN: {
                 startX = endX = rect.centerX();
-                final int vertCenter = rect.centerY();
-                final float halfGestureHeight = rect.height() / 2.0f;
-                startY = (int) (vertCenter + halfGestureHeight) - 1;
-                endY = (int) (vertCenter - halfGestureHeight);
+                startY = rect.bottom - 1;
+                endY = rect.top;
             }
             break;
             case LEFT: {
                 startY = endY = rect.centerY();
-                final int horizCenter = rect.centerX();
-                final float halfGestureWidth = rect.width() / 2.0f;
-                startX = (int) (horizCenter - halfGestureWidth) + 1;
-                endX = (int) (horizCenter + halfGestureWidth);
+                startX = rect.left;
+                endX = rect.right - 1;
             }
             break;
             case RIGHT: {
                 startY = endY = rect.centerY();
-                final int horizCenter = rect.centerX();
-                final float halfGestureWidth = rect.width() / 2.0f;
-                startX = (int) (horizCenter + halfGestureWidth) - 1;
-                endX = (int) (horizCenter - halfGestureWidth);
+                startX = rect.right - 1;
+                endX = rect.left;
             }
             break;
             default:
@@ -867,22 +1092,36 @@
         }
 
         executeAndWaitForEvent(
-                () -> linearGesture(startX, startY, endX, endY, steps),
+                () -> linearGesture(
+                        startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
-                "Didn't receive a scroll end message: " + startX + ", " + startY
+                () -> "Didn't receive a scroll end message: " + startX + ", " + startY
                         + ", " + endX + ", " + endY);
     }
 
     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
     // fixed interval each time.
-    void linearGesture(int startX, int startY, int endX, int endY, int steps) {
+    public void linearGesture(int startX, int startY, int endX, int endY, int steps,
+            boolean slowDown,
+            GestureScope gestureScope) {
         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
         final long downTime = SystemClock.uptimeMillis();
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
-        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
-        final long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, start, end);
-        sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end);
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
+        final long endTime = movePointer(start, end, steps, downTime, slowDown, gestureScope);
+        sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
+    }
+
+    long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown,
+            GestureScope gestureScope) {
+        long endTime = movePointer(
+                downTime, downTime, steps * GESTURE_STEP_MS, start, end, gestureScope);
+        if (slowDown) {
+            endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
+                    end, gestureScope);
+        }
+        return endTime;
     }
 
     void waitForIdle() {
@@ -915,13 +1154,40 @@
                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
     }
 
-    void sendPointer(long downTime, long currentTime, int action, Point point) {
+    public void sendPointer(long downTime, long currentTime, int action, Point point,
+            GestureScope gestureScope) {
+        final boolean notLauncher3 = !isLauncher3();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                if (gestureScope != GestureScope.OUTSIDE) {
+                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
+                }
+                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                    expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (notLauncher3 && gestureScope != GestureScope.INSIDE) {
+                    expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
+                }
+                if (gestureScope != GestureScope.OUTSIDE) {
+                    expectEvent(TestProtocol.SEQUENCE_MAIN, gestureScope == GestureScope.INSIDE
+                            ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+                }
+                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                    expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
+                }
+                break;
+        }
+
         final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
-        mInstrumentation.getUiAutomation().injectInputEvent(event, true);
+        assertTrue("injectInputEvent failed",
+                mInstrumentation.getUiAutomation().injectInputEvent(event, true));
         event.recycle();
     }
 
-    long movePointer(long downTime, long startTime, long duration, Point from, Point to) {
+    public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
+            GestureScope gestureScope) {
         log("movePointer: " + from + " to " + to);
         final Point point = new Point();
         long steps = duration / GESTURE_STEP_MS;
@@ -935,7 +1201,7 @@
             point.x = from.x + (int) (progress * (to.x - from.x));
             point.y = from.y + (int) (progress * (to.y - from.y));
 
-            sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
+            sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
         }
         return currentTime;
     }
@@ -944,6 +1210,19 @@
         return getSystemIntegerRes(context, "config_navBarInteractionMode");
     }
 
+    @NonNull
+    UiObject2 clickAndGet(
+            @NonNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent) {
+        final Point targetCenter = target.getVisibleCenter();
+        final long downTime = SystemClock.uptimeMillis();
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
+        expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
+        final UiObject2 result = waitForLauncherObject(resName);
+        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
+                GestureScope.INSIDE);
+        return result;
+    }
+
     private static int getSystemIntegerRes(Context context, String resName) {
         Resources res = context.getResources();
         int resId = res.getIdentifier(resName, "integer", "android");
@@ -994,6 +1273,32 @@
         getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
     }
 
+    public boolean hasAllAppsInOverview() {
+        // Vertical bar layouts don't contain all apps
+        if (!mDevice.isNaturalOrientation()) {
+            return false;
+        }
+        // Portrait two button (quickstep) always has all apps.
+        if (getNavigationModel() == NavigationModel.TWO_BUTTON) {
+            return true;
+        }
+        // Overview actions hide all apps
+        if (overviewActionsEnabled()) {
+            return false;
+        }
+        // ...otherwise there should be all apps
+        return true;
+    }
+
+    private boolean overviewActionsEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
+    private void disableSensorRotation() {
+        getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
+    }
+
     public void disableDebugTracing() {
         getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
     }
@@ -1003,6 +1308,11 @@
                 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    public Integer getPid() {
+        final Bundle testInfo = getTestInfo(TestProtocol.REQUEST_PID);
+        return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
+    }
+
     public void produceJavaLeak() {
         getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
     }
@@ -1010,4 +1320,76 @@
     public void produceNativeLeak() {
         getTestInfo(TestProtocol.REQUEST_NATIVE_LEAK);
     }
-}
\ No newline at end of file
+
+    public void produceViewLeak() {
+        getTestInfo(TestProtocol.REQUEST_VIEW_LEAK);
+    }
+
+    public ArrayList<ComponentName> getRecentTasks() {
+        ArrayList<ComponentName> tasks = new ArrayList<>();
+        ArrayList<String> components = getTestInfo(TestProtocol.REQUEST_RECENT_TASKS_LIST)
+                .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+        for (String s : components) {
+            tasks.add(ComponentName.unflattenFromString(s));
+        }
+        return tasks;
+    }
+
+    public void clearLauncherData() {
+        getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
+    }
+
+    public Closable eventsCheck() {
+        Assert.assertTrue("Nested event checking", mEventChecker == null);
+        disableSensorRotation();
+        final Integer initialPid = getPid();
+        final LogEventChecker eventChecker = new LogEventChecker(this);
+        if (eventChecker.start()) mEventChecker = eventChecker;
+
+        return () -> {
+            if (initialPid != null && initialPid.intValue() != getPid()) {
+                if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
+                checkForAnomaly();
+                Assert.fail(
+                        formatSystemHealthMessage(
+                                formatErrorWithEvents("Launcher crashed", false)));
+            }
+
+            if (mEventChecker != null) {
+                mEventChecker = null;
+                if (mCheckEventsForSuccessfulGestures) {
+                    final String message = eventChecker.verify(WAIT_TIME_MS, true);
+                    if (message != null) {
+                        dumpDiagnostics();
+                        checkForAnomaly();
+                        Assert.fail(formatSystemHealthMessage(
+                                "http://go/tapl : successful gesture produced " + message));
+                    }
+                } else {
+                    eventChecker.finishNoWait();
+                }
+            }
+        };
+    }
+
+    boolean isLauncher3() {
+        return "com.android.launcher3".equals(getLauncherPackageName());
+    }
+
+    void expectEvent(String sequence, Pattern expected) {
+        if (mEventChecker != null) {
+            mEventChecker.expectPattern(sequence, expected);
+        } else {
+            Log.d(TAG, "Expecting: " + sequence + " / " + expected);
+        }
+    }
+
+    Rect getVisibleBounds(UiObject2 object) {
+        try {
+            return object.getVisibleBounds();
+        } catch (Throwable t) {
+            fail(t.toString());
+            return null;
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
new file mode 100644
index 0000000..4440b82
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import android.os.SystemClock;
+
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to verify expected events.
+ */
+public class LogEventChecker {
+
+    private final LauncherInstrumentation mLauncher;
+
+    // Map from an event sequence name to an ordered list of expected events in that sequence.
+    private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
+
+    LogEventChecker(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+    }
+
+    boolean start() {
+        mExpectedEvents.clear();
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_START_EVENT_LOGGING) != null;
+    }
+
+    void expectPattern(String sequence, Pattern pattern) {
+        mExpectedEvents.add(sequence, pattern);
+    }
+
+    // Waits for the expected number of events and returns them.
+    private ListMap<String> finishSync(long waitForExpectedCountMs) {
+        final long startTime = SystemClock.uptimeMillis();
+        // Event strings with '/' separating the sequence and the event.
+        ArrayList<String> rawEvents;
+
+        while (true) {
+            rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS)
+                    .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+            final int expectedCount = mExpectedEvents.entrySet()
+                    .stream().mapToInt(e -> e.getValue().size()).sum();
+            if (rawEvents.size() >= expectedCount
+                    || SystemClock.uptimeMillis() > startTime + waitForExpectedCountMs) {
+                break;
+            }
+            SystemClock.sleep(100);
+        }
+
+        finishNoWait();
+
+        // Parse raw events into a map.
+        final ListMap<String> eventSequences = new ListMap<>();
+        for (String rawEvent : rawEvents) {
+            final String[] split = rawEvent.split("/");
+            eventSequences.add(split[0], split[1]);
+        }
+        return eventSequences;
+    }
+
+    void finishNoWait() {
+        mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING);
+    }
+
+    String verify(long waitForExpectedCountMs, boolean successfulGesture) {
+        final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
+
+        final StringBuilder sb = new StringBuilder();
+        boolean hasMismatches = false;
+        for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) {
+            String sequence = expectedEvents.getKey();
+
+            List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
+            final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
+            hasMismatches = hasMismatches
+                    || mismatchPosition != -1 && !ignoreMistatch(successfulGesture, sequence);
+            formatSequenceWithMismatch(
+                    sb,
+                    sequence,
+                    expectedEvents.getValue(),
+                    actual,
+                    mismatchPosition);
+        }
+        // Check for unexpected event sequences in the actual data.
+        for (String actualNamedSequence : actualEvents.keySet()) {
+            if (!mExpectedEvents.containsKey(actualNamedSequence)) {
+                hasMismatches = hasMismatches
+                        || !ignoreMistatch(successfulGesture, actualNamedSequence);
+                formatSequenceWithMismatch(
+                        sb,
+                        actualNamedSequence,
+                        new ArrayList<>(),
+                        actualEvents.get(actualNamedSequence),
+                        0);
+            }
+        }
+
+        return hasMismatches ? "mismatching events: " + sb.toString() : null;
+    }
+
+    // Workaround for b/154157191
+    private static boolean ignoreMistatch(boolean successfulGesture, String sequence) {
+        // b/156287114
+        return false;
+//        return TestProtocol.SEQUENCE_TIS.equals(sequence) && successfulGesture;
+    }
+
+    // If the list of actual events matches the list of expected events, returns -1, otherwise
+    // the position of the mismatch.
+    private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
+        for (int i = 0; i < expected.size(); ++i) {
+            if (i >= actual.size()
+                    || !expected.get(i).matcher(actual.get(i)).find()) {
+                return i;
+            }
+        }
+
+        if (actual.size() > expected.size()) return expected.size();
+
+        return -1;
+    }
+
+    private static void formatSequenceWithMismatch(
+            StringBuilder sb,
+            String sequenceName,
+            List<Pattern> expected,
+            List<String> actualEvents,
+            int mismatchPosition) {
+        sb.append("\n>> SEQUENCE " + sequenceName + " - "
+                + (mismatchPosition == -1 ? "MATCH" : "MISMATCH"));
+        sb.append("\n  EXPECTED:");
+        formatEventListWithMismatch(sb, expected, mismatchPosition);
+        sb.append("\n  ACTUAL:");
+        formatEventListWithMismatch(sb, actualEvents, mismatchPosition);
+    }
+
+    private static void formatEventListWithMismatch(StringBuilder sb, List events, int position) {
+        for (int i = 0; i < events.size(); ++i) {
+            sb.append("\n  | ");
+            sb.append(i == position ? "---> " : "     ");
+            sb.append(events.get(i).toString());
+        }
+        if (position == events.size()) sb.append("\n  | ---> (end)");
+    }
+
+    private static class ListMap<T> extends HashMap<String, List<T>> {
+
+        void add(String key, T value) {
+            getNonNull(key).add(value);
+        }
+
+        List<T> getNonNull(String key) {
+            List<T> list = get(key);
+            if (list == null) {
+                list = new ArrayList<>();
+                put(key, list);
+            }
+            return list;
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
new file mode 100644
index 0000000..282fca9
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+public class OptionsPopupMenu {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mDeepShortcutsContainer;
+
+    OptionsPopupMenu(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
+    }
+
+    /**
+     * Returns a menu item with a given label. Fails if it doesn't exist.
+     */
+    @NonNull
+    public OptionsPopupMenuItem getMenuItem(@NonNull final String label) {
+        final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
+                By.text(label));
+        return new OptionsPopupMenuItem(mLauncher, menuItem);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
new file mode 100644
index 0000000..42b6bc9
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.testing.TestProtocol;
+
+public class OptionsPopupMenuItem {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mObject;
+
+    OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) {
+        mLauncher = launcher;
+        mObject = shortcut;
+    }
+
+    /**
+     * Clicks the option.
+     */
+    @NonNull
+    public void launch(@NonNull String expectedPackageName) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+                    + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
+            mLauncher.clickLauncherObject(mObject);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
+            mLauncher.assertTrue(
+                    "App didn't start: " + By.pkg(expectedPackageName),
+                    mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+                            LauncherInstrumentation.WAIT_TIME_MS));
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index da68da3..4d673a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -45,8 +45,9 @@
      */
     @NonNull
     public AllAppsFromOverview switchToAllApps() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to switch from overview to all apps")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to switch from overview to all apps")) {
             verifyActiveContainer();
 
             // Swipe from an app icon to the top.
@@ -58,8 +59,9 @@
                             getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD),
                     mLauncher.getDevice().getDisplayWidth() / 2,
                     0,
-                    50,
-                    ALL_APPS_STATE_ORDINAL);
+                    12,
+                    ALL_APPS_STATE_ORDINAL,
+                    LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped all way up from overview")) {
@@ -67,4 +69,13 @@
             }
         }
     }
+
+    @Override
+    public void dismissAllTasks() {
+        super.dismissAllTasks();
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "dismissed all tasks")) {
+            new Workspace(mLauncher);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 91f0fc4..b235919 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -22,10 +22,16 @@
 
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.regex.Pattern;
+
 /**
  * A recent task in the overview panel carousel.
  */
 public final class OverviewTask {
+    static final Pattern TASK_START_EVENT =
+            Pattern.compile("startActivityFromRecentsAsync");
     private final LauncherInstrumentation mLauncher;
     private final UiObject2 mTask;
     private final BaseOverview mOverview;
@@ -45,14 +51,16 @@
      * Swipes the task up.
      */
     public void dismiss() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to dismiss a task")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to dismiss a task")) {
             verifyActiveContainer();
             // Dismiss the task via flinging it up.
-            final Rect taskBounds = mTask.getVisibleBounds();
+            final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
             final int centerX = taskBounds.centerX();
             final int centerY = taskBounds.centerY();
-            mLauncher.linearGesture(centerX, centerY, centerX, 0, 10);
+            mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
+                    LauncherInstrumentation.GestureScope.INSIDE);
             mLauncher.waitForIdle();
         }
     }
@@ -61,15 +69,18 @@
      * Clicks at the task.
      */
     public Background open() {
-        verifyActiveContainer();
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "clicking an overview task")) {
-            mLauncher.executeAndWaitForEvent(
-                    () -> mTask.click(),
-                    event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                    "Launching task didn't open a new window: " +
-                            mTask.getParent().getContentDescription());
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            verifyActiveContainer();
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "clicking an overview task")) {
+                mLauncher.executeAndWaitForEvent(
+                        () -> mLauncher.clickLauncherObject(mTask),
+                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                        () -> "Launching task didn't open a new window: "
+                                + mTask.getParent().getContentDescription());
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+            }
+            return new Background(mLauncher);
         }
-        return new Background(mLauncher);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index 0c9fda3..b8791e8 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -77,13 +77,18 @@
         return launchers.get(0).activityInfo;
     }
 
-    public static String getOverviewPackageName() {
+    public static ComponentName getOverviewComponentName() {
         Resources res = Resources.getSystem();
         int id = res.getIdentifier("config_recentsComponentName", "string", "android");
         if (id != 0) {
-            return ComponentName.unflattenFromString(res.getString(id)).getPackageName();
+            return ComponentName.unflattenFromString(res.getString(id));
         }
-        return "com.android.systemui";
+        return new ComponentName("com.android.systemui",
+                "com.android.systemui.recents.RecentsActivity");
+    }
+
+    public static String getOverviewPackageName() {
+        return getOverviewComponentName().getPackageName();
     }
 
     private static String truncateCrash(String text, int maxLines) {
@@ -146,8 +151,7 @@
                     ? "Current time: " + new Date(System.currentTimeMillis()) + "\n" + errors
                     : null;
         } catch (Exception e) {
-            return "Failed to get system health diags, maybe build your test via .bp instead of "
-                    + ".mk? " + android.util.Log.getStackTraceString(e);
+            return null;
         }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..53ef796 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -18,10 +18,17 @@
 
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
+import java.util.regex.Pattern;
+
 /**
  * Widget in workspace or a widget list.
  */
 public final class Widget extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("Widgets.onLongClick");
+
     Widget(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -30,4 +37,13 @@
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
+
+    @Override
+    protected void expectActivityStartEvents() {
+    }
+
+    @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 51239c9..39ac645 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+
 import android.graphics.Point;
 import android.graphics.Rect;
 
@@ -23,6 +25,9 @@
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.tapl.LauncherInstrumentation.GestureScope;
 
 import java.util.Collection;
 
@@ -41,15 +46,17 @@
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to fling forward in widgets")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to fling forward in widgets")) {
             LauncherInstrumentation.log("Widgets.flingForward enter");
             final UiObject2 widgetsContainer = verifyActiveContainer();
             mLauncher.scroll(
                     widgetsContainer,
                     Direction.DOWN,
-                    new Rect(0, 0, 0, mLauncher.getBottomGestureMargin(widgetsContainer)),
-                    FLING_STEPS);
+                    new Rect(0, 0, 0,
+                            mLauncher.getBottomGestureMarginInContainer(widgetsContainer) + 1),
+                    FLING_STEPS, false);
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung forward")) {
                 verifyActiveContainer();
             }
@@ -61,15 +68,16 @@
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to fling backwards in widgets")) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to fling backwards in widgets")) {
             LauncherInstrumentation.log("Widgets.flingBackward enter");
             final UiObject2 widgetsContainer = verifyActiveContainer();
             mLauncher.scroll(
                     widgetsContainer,
                     Direction.UP,
-                    new Rect(0, 0, widgetsContainer.getVisibleBounds().width(), 0),
-                    FLING_STEPS);
+                    new Rect(0, 0, mLauncher.getVisibleBounds(widgetsContainer).width(), 0),
+                    FLING_STEPS, false);
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung back")) {
                 verifyActiveContainer();
             }
@@ -83,33 +91,53 @@
     }
 
     public Widget getWidget(String labelText) {
-        final UiObject2 widgetsContainer = verifyActiveContainer();
-        final Point displaySize = mLauncher.getRealDisplaySize();
-        final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "getting widget " + labelText + " in widgets list")) {
+            final UiObject2 widgetsContainer = verifyActiveContainer();
+            mLauncher.assertTrue("Widgets container didn't become scrollable",
+                    widgetsContainer.wait(Until.scrollable(true), WAIT_TIME_MS));
+            final Point displaySize = mLauncher.getRealDisplaySize();
+            final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
 
-        int i = 0;
-        for (; ; ) {
-            final Collection<UiObject2> cells = mLauncher.getObjectsInContainer(
-                    widgetsContainer, "widgets_cell_list_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;
+            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());
+                    final UiObject2 widget = label.getParent().getParent();
+                    mLauncher.assertEquals(
+                            "View is not WidgetCell",
+                            "com.android.launcher3.widget.WidgetCell",
+                            widget.getClassName());
 
-                if (widget.getVisibleBounds().bottom <=
-                        displaySize.y - mLauncher.getBottomGestureSize()) {
-                    return new Widget(mLauncher, widget);
+                    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);
+                        }
+
+                        return new Widget(mLauncher, widget);
+                    }
                 }
-            }
 
-            mLauncher.assertTrue("Too many attempts", ++i <= 40);
-            mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
+                mLauncher.assertTrue("Too many attempts", ++i <= 40);
+                mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
+            }
         }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 0aa36dd..f0e686f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -17,9 +17,12 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
 
 import static junit.framework.TestCase.assertTrue;
 
+import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
@@ -32,14 +35,25 @@
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.testing.TestProtocol;
 
+import java.util.regex.Pattern;
+
 /**
  * Operations on the workspace screen.
  */
 public final class Workspace extends Home {
-    private static final int DRAG_DURACTION = 2000;
     private static final int FLING_STEPS = 10;
+
+    static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
+                    + ".*?metaState=META_CTRL_ON");
+    static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
+                    + ".*?metaState=META_CTRL_ON");
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
+
     private final UiObject2 mHotseat;
 
     Workspace(LauncherInstrumentation launcher) {
@@ -47,6 +61,34 @@
         mHotseat = launcher.waitForLauncherObject("hotseat");
     }
 
+    private static boolean supportsRoundedCornersOnWindows(Resources resources) {
+        return ResourceUtils.getBoolByName(
+                "config_supportsRoundedCornersOnWindows", resources, false);
+    }
+
+    private static float getWindowCornerRadius(Resources resources) {
+        if (!supportsRoundedCornersOnWindows(resources)) {
+            return 0f;
+        }
+
+        // Radius that should be used in case top or bottom aren't defined.
+        float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
+
+        float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
+        if (topRadius == 0f) {
+            topRadius = defaultRadius;
+        }
+        float bottomRadius = ResourceUtils.getDimenByName(
+                "rounded_corner_radius_bottom", resources, 0);
+        if (bottomRadius == 0f) {
+            bottomRadius = defaultRadius;
+        }
+
+        // Always use the smallest radius to make sure the rounded corners will
+        // completely cover the display.
+        return Math.min(topRadius, bottomRadius);
+    }
+
     /**
      * Swipes up to All Apps.
      *
@@ -54,12 +96,16 @@
      */
     @NonNull
     public AllApps switchToAllApps() {
-        try (LauncherInstrumentation.Closable c =
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
             verifyActiveContainer();
-            final UiObject2 hotseat = mHotseat;
-            final Point start = hotseat.getVisibleCenter();
-            start.y = hotseat.getVisibleBounds().bottom - 1;
+            final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
+            final int bottomGestureMargin = ResourceUtils.getNavbarSize(
+                    ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources());
+            final int windowCornerRadius = (int) Math.ceil(getWindowCornerRadius(
+                    mLauncher.getResources()));
+            final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
             final int swipeHeight = mLauncher.getTestInfo(
                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
@@ -68,12 +114,12 @@
                             + mLauncher.getTouchSlop());
 
             mLauncher.swipeToState(
-                    start.x,
-                    start.y,
-                    start.x,
-                    start.y - swipeHeight - mLauncher.getTouchSlop(),
-                    60,
-                    ALL_APPS_STATE_ORDINAL);
+                    0,
+                    startY,
+                    0,
+                    startY - swipeHeight - mLauncher.getTouchSlop(),
+                    12,
+                    ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped to all apps")) {
@@ -122,21 +168,27 @@
      * second screen.
      */
     public void ensureWorkspaceIsScrollable() {
-        final UiObject2 workspace = verifyActiveContainer();
-        if (!isWorkspaceScrollable(workspace)) {
-            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                    "dragging icon to a second page of workspace to make it scrollable")) {
-                dragIconToWorkspace(
-                        mLauncher,
-                        getHotseatAppIcon("Chrome"),
-                        new Point(mLauncher.getDevice().getDisplayWidth(),
-                                workspace.getVisibleBounds().centerY()),
-                        "deep_shortcuts_container");
-                verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            if (!isWorkspaceScrollable(workspace)) {
+                try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                        "dragging icon to a second page of workspace to make it scrollable")) {
+                    dragIconToWorkspace(
+                            mLauncher,
+                            getHotseatAppIcon("Chrome"),
+                            new Point(mLauncher.getDevice().getDisplayWidth(),
+                                    mLauncher.getVisibleBounds(workspace).centerY()),
+                            "deep_shortcuts_container",
+                            false,
+                            false,
+                            () -> mLauncher.expectEvent(
+                                    TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
+                    verifyActiveContainer();
+                }
             }
+            assertTrue("Home screen workspace didn't become scrollable",
+                    isWorkspaceScrollable(workspace));
         }
-        assertTrue("Home screen workspace didn't become scrollable",
-                isWorkspaceScrollable(workspace));
     }
 
     private boolean isWorkspaceScrollable(UiObject2 workspace) {
@@ -149,29 +201,36 @@
                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
     }
 
-    @NonNull
-    public Folder getHotseatFolder(String appName) {
-        return new Folder(mLauncher, mLauncher.waitForObjectInContainer(
-                mHotseat, Folder.getSelector(appName, mLauncher)));
-    }
-
     static void dragIconToWorkspace(
             LauncherInstrumentation launcher, Launchable launchable, Point dest,
-            String longPressIndicator) {
+            String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut,
+            Runnable expectLongClickEvents) {
         LauncherInstrumentation.log("dragIconToWorkspace: begin");
         final Point launchableCenter = launchable.getObject().getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
-        launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, launchableCenter);
-        LauncherInstrumentation.log("dragIconToWorkspace: sent down");
-        launcher.waitForLauncherObject(longPressIndicator);
-        LauncherInstrumentation.log("dragIconToWorkspace: indicator");
-        launcher.movePointer(
-                downTime, SystemClock.uptimeMillis(), DRAG_DURACTION, launchableCenter, dest);
+        launcher.runToState(
+                () -> {
+                    launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
+                            launchableCenter, LauncherInstrumentation.GestureScope.INSIDE);
+                    LauncherInstrumentation.log("dragIconToWorkspace: sent down");
+                    expectLongClickEvents.run();
+                    launcher.waitForLauncherObject(longPressIndicator);
+                    LauncherInstrumentation.log("dragIconToWorkspace: indicator");
+                    launcher.movePointer(launchableCenter, dest, 10, downTime, true,
+                            LauncherInstrumentation.GestureScope.INSIDE);
+                },
+                SPRING_LOADED_STATE_ORDINAL);
         LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
-        launcher.sendPointer(
-                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest);
+        launcher.runToState(
+                () -> launcher.sendPointer(
+                        downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
+                        LauncherInstrumentation.GestureScope.INSIDE),
+                NORMAL_STATE_ORDINAL);
+        if (startsActivity || isWidgetShortcut) {
+            launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
+        }
         LauncherInstrumentation.log("dragIconToWorkspace: end");
-        launcher.waitUntilGone("drop_target_bar");
+        launcher.waitUntilLauncherObjectGone("drop_target_bar");
     }
 
     /**
@@ -179,11 +238,13 @@
      * recoil to complete.
      */
     public void flingForward() {
-        final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.scroll(workspace, Direction.RIGHT,
-                new Rect(0, 0, mLauncher.getEdgeSensitivityWidth(), 0),
-                FLING_STEPS);
-        verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            mLauncher.scroll(workspace, Direction.RIGHT,
+                    new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
+                    FLING_STEPS, false);
+            verifyActiveContainer();
+        }
     }
 
     /**
@@ -191,11 +252,13 @@
      * recoil to complete.
      */
     public void flingBackward() {
-        final UiObject2 workspace = verifyActiveContainer();
-        mLauncher.scroll(workspace, Direction.LEFT,
-                new Rect(mLauncher.getEdgeSensitivityWidth(), 0, 0, 0),
-                FLING_STEPS);
-        verifyActiveContainer();
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            mLauncher.scroll(workspace, Direction.LEFT,
+                    new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
+                    FLING_STEPS, false);
+            verifyActiveContainer();
+        }
     }
 
     /**
@@ -205,10 +268,14 @@
      */
     @NonNull
     public Widgets openAllWidgets() {
-        verifyActiveContainer();
-        mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
-            return new Widgets(mLauncher);
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            verifyActiveContainer();
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP);
+            mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
+                return new Widgets(mLauncher);
+            }
         }
     }
 
@@ -224,16 +291,22 @@
 
     @Nullable
     public Widget tryGetWidget(String label, long timeout) {
-        final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
-                By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label),
-                timeout);
-        return widget != null ? new Widget(mLauncher, widget) : null;
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "getting widget " + label + " on workspace with timeout " + timeout)) {
+            final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
+                    By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label),
+                    timeout);
+            return widget != null ? new Widget(mLauncher, widget) : null;
+        }
     }
 
     @Nullable
     public Widget tryGetPendingWidget(long timeout) {
-        final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
-                By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
-        return widget != null ? new Widget(mLauncher, widget) : null;
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "getting pending widget on workspace with timeout " + timeout)) {
+            final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
+                    By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
+            return widget != null ? new Widget(mLauncher, widget) : null;
+        }
     }
 }
\ No newline at end of file
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..0f4163d
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+  <!ENTITY defaultCopyrightCheck SYSTEM "../../../../prebuilts/checkstyle/default-copyright-check.xml">
+  <!ENTITY defaultJavadocChecks SYSTEM "../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+  <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+  <!ENTITY defaultModuleChecks SYSTEM "../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+  &defaultModuleChecks;
+  &defaultCopyrightCheck;
+  <module name="TreeWalker">
+    &defaultJavadocChecks;
+    &defaultTreewalkerChecks;
+  </module>
+
+  <module name="SuppressionFilter">
+    <property name="file" value="tools/checkstyle_suppression.xml" />
+  </module>
+</module>
diff --git a/tools/checkstyle_suppression.xml b/tools/checkstyle_suppression.xml
new file mode 100644
index 0000000..799e750
--- /dev/null
+++ b/tools/checkstyle_suppression.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+    <!-- Robolectric uses magic method names like `__constructor__` -->
+    <suppress files="/robolectric_tests" checks="MethodName|JavadocType|JavadocMethod" />
+
+</suppressions>
diff --git a/print_db.py b/tools/print_db.py
similarity index 100%
rename from print_db.py
rename to tools/print_db.py