Merge "Revert "Temporarily disabling events checking in git_master""
diff --git a/Android.mk b/Android.mk
index 9cfcf17..7805b32 100644
--- a/Android.mk
+++ b/Android.mk
@@ -67,7 +67,10 @@
 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_ui_overrides) \
+    $(call all-java-files-under, ext_tests/src)
+    
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/ext_tests/res
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 # Proguard is disable for testing. Derivarive prjects to keep proguard enabled
@@ -129,6 +132,7 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
+    SystemUI-statsd \
     SystemUISharedLib \
     launcherprotosnano \
     launcher_log_protos_lite
@@ -201,6 +205,7 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
+    SystemUI-statsd \
     SystemUISharedLib \
     launcherprotosnano \
     launcher_log_protos_lite
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index d7191b4..84dd06a 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -86,6 +86,7 @@
         <receiver
             android:name="com.android.launcher3.InstallShortcutReceiver"
             android:permission="com.android.launcher.permission.INSTALL_SHORTCUT"
+            android:exported="true"
             android:enabled="@bool/enable_install_shortcut_api" >
             <intent-filter>
                 <action android:name="com.android.launcher.action.INSTALL_SHORTCUT" />
@@ -94,14 +95,16 @@
 
         <!-- Intent received when a session is committed -->
         <receiver
-            android:name="com.android.launcher3.SessionCommitReceiver" >
+            android:name="com.android.launcher3.SessionCommitReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.content.pm.action.SESSION_COMMITTED" />
             </intent-filter>
         </receiver>
 
         <!-- Intent received used to initialize a restored widget -->
-        <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver" >
+        <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED"/>
             </intent-filter>
@@ -117,6 +120,7 @@
             android:name="com.android.launcher3.notification.NotificationListener"
             android:label="@string/notification_dots_service_title"
             android:enabled="@bool/notification_dots_enabled"
+            android:exported="true"
             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
                 <action android:name="android.service.notification.NotificationListenerService" />
@@ -130,6 +134,7 @@
             android:theme="@style/AppItemActivityTheme"
             android:excludeFromRecents="true"
             android:autoRemoveFromRecents="true"
+            android:exported="true"
             android:label="@string/action_add_to_workspace" >
             <intent-filter>
                 <action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
@@ -156,7 +161,6 @@
         <provider
             android:name="com.android.launcher3.graphics.GridOptionsProvider"
             android:authorities="${packageName}.grid_control"
-            android:enabled="false"
             android:exported="true" />
 
         <!--
@@ -166,6 +170,7 @@
             android:name="com.android.launcher3.settings.SettingsActivity"
             android:label="@string/settings_button_text"
             android:theme="@style/HomeSettingsTheme"
+            android:exported="true"
             android:autoRemoveFromRecents="true">
             <intent-filter>
                 <action android:name="android.intent.action.APPLICATION_PREFERENCES" />
@@ -188,6 +193,7 @@
             android:name="com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher"
             android:theme="@style/AppTheme"
             android:launchMode="singleTop"
+            android:exported="true"
             android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b031ffb..8e01f82 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,6 +53,7 @@
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
+            android:exported="true"
             android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
deleted file mode 100644
index e69de29..0000000
--- a/MODULE_LICENSE_APACHE2
+++ /dev/null
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/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/protos/launcher_atom.proto b/protos/launcher_atom.proto
index a89fe5c..5611969 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -23,9 +23,10 @@
 message ItemInfo {
   oneof Item {
     Application application = 1;
-    Task task= 2;
+    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;
@@ -34,16 +35,67 @@
   optional bool is_work = 6;
 
   // Item can be child node to parent container or parent containers (nested)
-  oneof Container {
-    WorkspaceContainer workspace = 7;
-    HotseatContainer hotseat = 8;
-    FolderContainer folder = 9;
-  }
+  optional ContainerInfo container_info = 7;
+
   // Stores the origin of the Item
-  optional Origin source = 10;
+  optional Attribute attribute = 8;
 }
 
-enum Origin {
+// 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
@@ -53,6 +105,21 @@
   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
@@ -68,8 +135,8 @@
 
 // AppWidgets handled by AppWidgetManager
 message Widget {
-  optional int32 span_x = 1;
-  optional int32 span_y = 2;
+  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
@@ -82,27 +149,124 @@
   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; // range [-1, l], 0 is the index of the main homescreen
-  optional int32 grid_x = 2;     // [0, m], m varies based on the display density and resolution
-  optional int32 grid_y = 3;     // [0, n], n varies based on the display density and resolution
+  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;
-  optional int32 grid_x = 2;
-  optional int32 grid_y = 3;
-  oneof Container {
+  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_log.proto b/protos/launcher_log.proto
index 0fe5310..9423cb2 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -63,14 +63,14 @@
 
   // Note: proto does not support duplicate enum values, even if they belong to different enum type.
   // Hence "FROM" and "TO" prefix added.
-  enum FromFolderLabelState{
+  enum FromFolderLabelState {
     FROM_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
     FROM_EMPTY = 1;
     FROM_CUSTOM = 2;
     FROM_SUGGESTED = 3;
   }
 
-  enum ToFolderLabelState{
+  enum ToFolderLabelState {
     TO_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
     TO_SUGGESTION0_WITH_VALID_PRIMARY = 1;
     TO_SUGGESTION1_WITH_VALID_PRIMARY = 2;
@@ -79,10 +79,14 @@
     TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 5;
     TO_SUGGESTION3_WITH_VALID_PRIMARY = 6;
     TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 7;
-    TO_EMPTY_WITH_VALID_SUGGESTIONS = 8;
+    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;
+    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;
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 527bfc3..1a634ce 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -52,8 +52,9 @@
             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
-            android:taskAffinity="${packageName}.launcher"
-            android:enabled="true">
+            android:taskAffinity=""
+            android:enabled="true"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.HOME" />
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 04506b5..868c659 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -17,98 +17,104 @@
 ** limitations under the License.
 */
 -->
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="com.android.launcher3" >
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     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"
-        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" >
+    <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">
 
-        <service
-            android:name="com.android.quickstep.TouchInteractionService"
-            android:permission="android.permission.STATUS_BAR_SERVICE"
-            android:directBootAware="true" >
+        <service android:name="com.android.quickstep.TouchInteractionService"
+             android:permission="android.permission.STATUS_BAR_SERVICE"
+             android:directBootAware="true"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.QUICKSTEP_SERVICE" />
+                <action android:name="android.intent.action.QUICKSTEP_SERVICE"/>
             </intent-filter>
         </service>
 
         <activity android:name="com.android.quickstep.RecentsActivity"
-            android:excludeFromRecents="true"
-            android:launchMode="singleTask"
-            android:clearTaskOnLaunch="true"
-            android:stateNotNeeded="true"
-            android:theme="@style/LauncherTheme"
-            android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
-            android:resizeableActivity="true"
-            android:resumeWhilePausing="true"
-            android:taskAffinity="" />
+             android:excludeFromRecents="true"
+             android:launchMode="singleTask"
+             android:clearTaskOnLaunch="true"
+             android:stateNotNeeded="true"
+             android:theme="@style/LauncherTheme"
+             android:screenOrientation="unspecified"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:resizeableActivity="true"
+             android:resumeWhilePausing="true"
+             android:taskAffinity=""/>
 
         <!-- Content provider to settings search. The autority should be same as the packageName -->
-        <provider
-            android:name="com.android.quickstep.LauncherSearchIndexablesProvider"
-            android:authorities="${packageName}"
-            android:grantUriPermissions="true"
-            android:multiprocess="true"
-            android:permission="android.permission.READ_SEARCH_INDEXABLES"
-            android:exported="true">
+        <provider android:name="com.android.quickstep.LauncherSearchIndexablesProvider"
+             android:authorities="${packageName}"
+             android:grantUriPermissions="true"
+             android:multiprocess="true"
+             android:permission="android.permission.READ_SEARCH_INDEXABLES"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" />
+                <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER"/>
             </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 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" />
+        <service android:name="com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL$ColorExtractionService"
+             tools:node="remove"/>
 
-        <activity
-            android:name="com.android.launcher3.proxy.ProxyActivityStarter"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
-            android:launchMode="singleTask"
-            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.launcher3.proxy.ProxyActivityStarter"
+             android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
+             android:launchMode="singleTask"
+             android:clearTaskOnLaunch="true"
+             android:exported="false"/>
 
         <activity
             android:name="com.android.quickstep.interaction.GestureSandboxActivity"
             android:autoRemoveFromRecents="true"
             android:excludeFromRecents="true"
-            android:taskAffinity="${packageName}.launcher"
-            android:screenOrientation="portrait">
+            android:screenOrientation="portrait"
+            android:exported="true">
             <intent-filter>
-                <action android:name="com.android.quickstep.action.GESTURE_SANDBOX" />
+                <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"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU"/>
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
diff --git a/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
new file mode 100644
index 0000000..df7cd8e
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    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/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 ffe906c..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,24 +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" />
+        android:clipChildren="false">
 
-    <include
-        android:id="@+id/overview_actions_view"
-        layout="@layout/overview_actions_container" />
+        <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" />
 
-</com.android.quickstep.fallback.RecentsRootView>
+        <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 eac0bfa..fe57e9b 100644
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
@@ -13,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: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" />
+<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/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
index c93cad6..1dab482 100644
--- a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
@@ -24,13 +24,13 @@
     <View
         android:layout_width="match_parent"
         android:layout_height="32dp"
-        android:backgroundTint="?android:attr/colorAccent"
+        android:backgroundTint="?attr/eduHalfSheetBGColor"
         android:background="@drawable/bottom_sheet_top_border" />
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="?android:attr/colorAccent"
+        android:background="?attr/eduHalfSheetBGColor"
         android:orientation="vertical">
 
         <TextView
@@ -72,33 +72,43 @@
                 android:layout_height="0dp"
                 launcher:containerType="hotseat" />
 
-            <FrameLayout
+            <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">
 
-                <Button
-                    android:id="@+id/turn_predictions_on"
-                    android:layout_width="wrap_content"
+                <FrameLayout
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="end"
-                    android:background="?android:attr/selectableItemBackground"
-                    android:text="@string/hotseat_edu_accept"
-                    android:textAlignment="textEnd"
-                    android:textColor="@android:color/white" />
-
-                <Button
-                    android:layout_width="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:id="@+id/no_thanks"
-                    android:text="@string/hotseat_edu_dismiss"
-                    android:layout_gravity="start"
-                    android:background="?android:attr/selectableItemBackground"
-                    android:textColor="@android:color/white" />
+                    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>
 
-            </FrameLayout>
+            </LinearLayout>
         </LinearLayout>
     </LinearLayout>
 
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 361f5f7..f03f118 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -15,6 +15,7 @@
 -->
 <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/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index 363840a..9266b06 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -20,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>
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 0019ecb..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.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.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.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+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.util.FloatProperty;
 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.Interpolators;
-import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.quickstep.util.AppWindowAnimationHelper;
+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);
     }
@@ -87,19 +64,17 @@
         boolean skipLauncherChanges = !launcherClosing;
 
         TaskView taskView = findTaskViewToLaunch(mLauncher, v, appTargets);
-
-        AppWindowAnimationHelper helper =
-            new AppWindowAnimationHelper(recentsView.getPagedViewOrientedState(), mLauncher);
-        Animator recentsAnimator = getRecentsWindowAnimator(taskView, skipLauncherChanges,
-                appTargets, wallpaperTargets, mLauncher.getDepthController(), helper);
-        anim.play(recentsAnimator.setDuration(RECENTS_LAUNCH_DURATION));
+        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);
 
@@ -152,81 +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:
-                PagedOrientationHandler orientationHandler =
-                    ((RecentsView)mLauncher.getOverviewPanel()).getPagedViewOrientedState()
-                        .getOrientationHandler();
-                FloatProperty<View> translate = orientationHandler.getPrimaryViewTranslate();
-                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), translate)
-                        .setDampingRatio(0.8f)
-                        .setStiffness(250)
-                        .setValues(values)
-                        .build(mLauncher);
-            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(mLauncher) & HOTSEAT_ICONS) != 0) {
-                    config.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
-                    config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
-                }
-
-
-                LauncherStateManager stateManager = mLauncher.getStateManager();
-                return stateManager.createAtomicAnimation(
-                        stateManager.getCurrentStableState(), OVERVIEW, config);
-            }
-
-            default:
-                return super.createStateElementAnimation(index, values);
-        }
-    }
 }
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 079a738..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
@@ -26,10 +26,10 @@
 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.statemanager.StateManager.StateListener;
 import com.android.launcher3.views.ArrowTipView;
 import com.android.systemui.shared.system.LauncherEventUtil;
 
@@ -71,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 ec46418..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
@@ -38,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";
@@ -251,9 +251,6 @@
     }
 
     @Override
-    public void onStateTransitionStart(LauncherState toState) { }
-
-    @Override
     public void onStateTransitionComplete(LauncherState finalState) {
         if (finalState == ALL_APPS) {
             setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
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 0712285..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,10 +18,9 @@
 
 import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
-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 {
@@ -57,9 +56,8 @@
             return item;
         } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
             return mCache.getInstantApp(componentKey.componentName.getPackageName());
-        } else if (componentKey instanceof ShortcutKey) {
-            return mCache.getShortcutInfo((ShortcutKey) componentKey);
+        } else {
+            return mCache.getShortcutInfo(componentKey);
         }
-        return null;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 54f58e2..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
@@ -38,13 +38,14 @@
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.AppInfo;
 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.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;
@@ -76,7 +77,7 @@
     private final Runnable mOnUpdateCallback;
     private final IconCache mIconCache;
 
-    private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
+    private final Map<ComponentKey, WorkspaceItemInfo> mShortcuts;
     private final Map<String, InstantAppItemInfo> mInstantApps;
 
     public DynamicItemCache(Context context, Runnable onUpdateCallback) {
@@ -230,7 +231,7 @@
     }
 
     @MainThread
-    public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+    public WorkspaceItemInfo getShortcutInfo(ComponentKey key) {
         return mShortcuts.get(key);
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/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 8faec46..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
@@ -31,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;
@@ -38,18 +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;
@@ -60,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;
@@ -241,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);
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 632b9b5..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
@@ -16,6 +16,9 @@
 
 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;
 
@@ -25,22 +28,23 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 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.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;
@@ -48,6 +52,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.OptionalInt;
 import java.util.stream.IntStream;
 
 /**
@@ -63,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";
 
@@ -158,9 +163,6 @@
     public void reapplyItemInfo(ItemInfoWithIcon info) { }
 
     @Override
-    public void onStateTransitionStart(LauncherState toState) { }
-
-    @Override
     public void onStateTransitionComplete(LauncherState state) {
         if (mAppsView == null) {
             return;
@@ -214,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 {
@@ -302,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},
@@ -310,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
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
index 773c6c8..4f95254 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -15,56 +15,46 @@
  */
 package com.android.launcher3.hybridhotseat;
 
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+        .LAUNCHER_HOTSEAT_EDU_ONLY_TIP;
+
 import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Build;
 import android.view.View;
 
-import androidx.core.app.NotificationCompat;
-
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.ActivityTracker;
+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.util.Themes;
 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 KEY_HOTSEAT_EDU_SEEN = "hotseat_edu_seen";
 
-    private static final String NOTIFICATION_CHANNEL_ID = "launcher_onboarding";
-    private static final int ONBOARDING_NOTIFICATION_ID = 7641;
-
-    private static final String SETTINGS_ACTION =
+    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 final NotificationManager mNotificationManager;
-    private final Notification mNotification;
+    private HotseatRestoreHelper mRestoreHelper;
     private List<WorkspaceItemInfo> mPredictedApps;
     private HotseatEduDialog mActiveDialog;
 
@@ -72,24 +62,28 @@
     private IntArray mNewScreens = null;
     private Runnable mOnOnboardingComplete;
 
-    HotseatEduController(Launcher launcher, Runnable runnable) {
+    HotseatEduController(Launcher launcher, HotseatRestoreHelper restoreHelper, Runnable runnable) {
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
+        mRestoreHelper = restoreHelper;
         mOnOnboardingComplete = runnable;
-        mNotificationManager = mLauncher.getSystemService(NotificationManager.class);
-        createNotificationChannel();
-        mNotification = createNotification();
     }
 
     /**
      * 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()));
     }
 
     /**
@@ -100,7 +94,6 @@
      */
     private int migrateToFolder() {
         ArrayDeque<FolderInfo> folders = new ArrayDeque<>();
-
         ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
 
         //separate folders and items that can get in folders
@@ -110,7 +103,8 @@
             ItemInfo info = (ItemInfo) view.getTag();
             if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
                 folders.add((FolderInfo) info);
-            } else if (info instanceof WorkspaceItemInfo) {
+            } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings
+                    .Favorites.CONTAINER_HOTSEAT) {
                 putIntoFolder.add((WorkspaceItemInfo) info);
             }
         }
@@ -119,9 +113,9 @@
         if (!putIntoFolder.isEmpty()) {
             ItemInfo firstItem = putIntoFolder.get(0);
             FolderInfo folderInfo = new FolderInfo();
-            folderInfo.title = "";
             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);
@@ -200,11 +194,13 @@
             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);
@@ -212,11 +208,6 @@
         return pageId;
     }
 
-
-    void removeNotification() {
-        mNotificationManager.cancel(ONBOARDING_NOTIFICATION_ID);
-    }
-
     void moveHotseatItems() {
         mHotseat.removeAllViewsInLayout();
         if (!mNewItems.isEmpty()) {
@@ -237,15 +228,14 @@
 
     void finishOnboarding() {
         mOnOnboardingComplete.run();
-        destroy();
-        mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
     }
 
     void showDimissTip() {
         if (mHotseat.getShortcutsAndWidgets().getChildCount()
                 < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
-            Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, R.string.hotseat_turn_off,
-                    null, () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
+            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());
@@ -254,58 +244,27 @@
 
     void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
         mPredictedApps = predictedApps;
-        if (!mPredictedApps.isEmpty()
-                && mLauncher.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
-            mNotificationManager.notify(ONBOARDING_NOTIFICATION_ID, mNotification);
-        }
-        else {
-            removeNotification();
-        }
-    }
-
-    private void createNotificationChannel() {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
-        CharSequence name = mLauncher.getString(R.string.hotseat_edu_prompt_title);
-        int importance = NotificationManager.IMPORTANCE_LOW;
-        NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name,
-                importance);
-        mNotificationManager.createNotificationChannel(channel);
-    }
-
-    private Notification createNotification() {
-        Intent intent = new Intent(mLauncher.getApplicationContext(), mLauncher.getClass());
-        intent = new NotificationHandler().addToIntent(intent);
-
-        CharSequence name = mLauncher.getString(R.string.hotseat_edu_prompt_title);
-        String description = mLauncher.getString(R.string.hotseat_edu_prompt_content);
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(mLauncher,
-                NOTIFICATION_CHANNEL_ID)
-                .setContentTitle(name)
-                .setOngoing(true)
-                .setColor(Themes.getColorAccent(mLauncher))
-                .setContentIntent(PendingIntent.getActivity(mLauncher, 0, intent,
-                        PendingIntent.FLAG_CANCEL_CURRENT))
-                .setSmallIcon(R.drawable.hotseat_edu_notification_icon)
-                .setContentText(description);
-        return builder.build();
-
-    }
-
-    void destroy() {
-        removeNotification();
-        if (mActiveDialog != null) {
-            mActiveDialog.setHotseatEduController(null);
-        }
     }
 
     void 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
-        if (mHotseat.getShortcutsAndWidgets().getChildCount() == 0) {
-            new ArrowTipView(mLauncher).show(mLauncher.getString(R.string.hotseat_auto_enrolled),
-                    mHotseat.getTop());
-            finishOnboarding();
-        } else {
+        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();
         }
     }
 
@@ -321,13 +280,8 @@
         mActiveDialog.show(mPredictedApps);
     }
 
-    static class NotificationHandler implements
-            ActivityTracker.SchedulerCallback<QuickstepLauncher> {
-        @Override
-        public boolean init(QuickstepLauncher activity, boolean alreadyOnHome) {
-            activity.getHotseatPredictionController().showEdu();
-            return true;
-        }
+    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
index cb46c40..4b8e434 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -15,9 +15,10 @@
  */
 package com.android.launcher3.hybridhotseat;
 
-import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType
-        .HYBRID_HOTSEAT_CANCELED;
+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;
@@ -29,15 +30,15 @@
 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.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logging.UserEventDispatcher;
+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;
@@ -110,15 +111,13 @@
 
         mHotseatEduController.moveHotseatItems();
         mHotseatEduController.finishOnboarding();
-        //TODO: pass actual page index here.
-        // Temporarily we're passing 1 for folder migration and 2 for page migration
-        logUserAction(true, FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get() ? 1 : 2);
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ACCEPT);
     }
 
     private void onDismiss(View v) {
         mHotseatEduController.showDimissTip();
         mHotseatEduController.finishOnboarding();
-        logUserAction(false, -1);
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_DENY);
         handleClose(true);
     }
 
@@ -143,38 +142,23 @@
         int rightInset = insets.right - mInsets.right;
         int bottomInset = insets.bottom - mInsets.bottom;
         mInsets.set(insets);
-        setPadding(leftInset, getPaddingTop(), rightInset, 0);
-        mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
-                mHotseatWrapper.getPaddingRight(), bottomInset);
-        mHotseatWrapper.getLayoutParams().height =
-                mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
-    }
+        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;
 
-    private void logUserAction(boolean migrated, int pageIndex) {
-        LauncherLogProto.Action action = new LauncherLogProto.Action();
-        LauncherLogProto.Target target = new LauncherLogProto.Target();
-        action.type = LauncherLogProto.Action.Type.TOUCH;
-        action.touch = LauncherLogProto.Action.Touch.TAP;
-        target.containerType = LauncherLogProto.ContainerType.TIP;
-        target.tipType = LauncherLogProto.TipType.HYBRID_HOTSEAT;
-        target.controlType = migrated ? LauncherLogProto.ControlType.HYBRID_HOTSEAT_ACCEPTED
-                : HYBRID_HOTSEAT_CANCELED;
-        target.rank = MIGRATION_EXPERIMENT_IDENTIFIER;
-        // encoding migration type on pageIndex
-        target.pageIndex = pageIndex;
-        target.cardinality = HotseatPredictionController.MAX_ITEMS_FOR_MIGRATION;
-        LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
-        UserEventDispatcher.newInstance(getContext()).dispatchUserEvent(event, null);
-    }
-
-    private void logOnBoardingSeen() {
-        LauncherLogProto.Action action = new LauncherLogProto.Action();
-        LauncherLogProto.Target target = new LauncherLogProto.Target();
-        action.type = LauncherLogProto.Action.Type.TIP;
-        target.containerType = LauncherLogProto.ContainerType.TIP;
-        target.tipType = LauncherLogProto.TipType.HYBRID_HOTSEAT;
-        LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
-        UserEventDispatcher.newInstance(getContext()).dispatchUserEvent(event, null);
+        } 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() {
@@ -225,8 +209,9 @@
                 || mHotseatEduController == null) {
             return;
         }
+        AbstractFloatingView.closeAllOpenViews(mLauncher);
         attachToContainer();
-        logOnBoardingSeen();
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_SEEN);
         animateOpen();
         populatePreview(predictions);
     }
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
index 9bc0975..b94e633 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -15,10 +15,11 @@
  */
 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.logging.LoggerUtils.newAction;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
+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;
@@ -28,33 +29,25 @@
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
-import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
-import android.os.Bundle;
-import android.provider.DeviceConfig;
+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.AppInfo;
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.appprediction.ComponentKeyMapper;
@@ -62,17 +55,27 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.logging.UserEventDispatcher;
+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.DeviceFlag;
 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;
@@ -91,18 +94,6 @@
     private static final String TAG = "PredictiveHotseat";
     private static final boolean DEBUG = false;
 
-    public static final int MAX_ITEMS_FOR_MIGRATION = DeviceConfig.getInt(
-            DeviceFlag.NAMESPACE_LAUNCHER, "max_homepage_items_for_migration", 5);
-
-    //TODO: replace this with AppTargetEvent.ACTION_UNPIN (b/144119543)
-    private static final int APPTARGET_ACTION_UNPIN = 4;
-
-    private static final String APP_LOCATION_HOTSEAT = "hotseat";
-    private static final String APP_LOCATION_WORKSPACE = "workspace";
-
-    private static final String BUNDLE_KEY_HOTSEAT = "hotseat_apps";
-    private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
-
     private static final String PREDICTION_CLIENT = "hotseat";
     private DropTarget.DragObject mDragObject;
     private int mHotSeatItemsCount;
@@ -111,16 +102,18 @@
     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 HotseatEduController mHotseatEduController;
+    private boolean mIsDestroyed = false;
 
 
     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
@@ -128,40 +121,78 @@
     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 false;
+        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);
         }
     }
 
     /**
-     * Returns whether or not the prediction controller is ready to show predictions
+     * Shows appropriate hotseat education based on prediction enabled and migration states.
      */
-    public boolean isReady() {
-        return mLauncher.getSharedPrefs().getBoolean(HotseatEduController.KEY_HOTSEAT_EDU_SEEN,
-                false);
+    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();
+            }
+        });
     }
 
     /**
-     * Transitions to NORMAL workspace mode and shows edu
+     * Shows educational tip for hotseat if user does not go through Tips app.
      */
-    public void showEdu() {
-        if (mHotseatEduController == null) return;
-        mLauncher.getStateManager().goToState(LauncherState.NORMAL, true,
-                () -> mHotseatEduController.showEdu());
+    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
@@ -179,10 +210,15 @@
     }
 
     private void fillGapsWithPrediction(boolean animate, Runnable callback) {
-        if (!isReady() || mUIUpdatePaused || mDragObject != null) {
+        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
@@ -249,6 +285,7 @@
      * Unregisters callbacks and frees resources
      */
     public void destroy() {
+        mIsDestroyed = true;
         mAllAppsStore.removeUpdateListener(this);
         mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
         mHotseat.removeOnAttachStateChangeListener(this);
@@ -277,49 +314,61 @@
         }
         if (mAppPredictor != null) {
             mAppPredictor.destroy();
+            mAppPredictor = null;
         }
-        mAppPredictor = apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(mLauncher)
-                        .setUiSurface(PREDICTION_CLIENT)
-                        .setPredictedTargetCount(mHotSeatItemsCount)
-                        .setExtras(getAppPredictionContextExtra())
-                        .build());
-        mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
-                this::setPredictedApps);
+        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);
-        performBetaCheck();
-        if (!isReady()) {
-            mHotseatEduController = new HotseatEduController(mLauncher, this::createPredictor);
-        }
-        mAppPredictor.requestPredictionUpdate();
     }
 
-    private Bundle getAppPredictionContextExtra() {
-        Bundle bundle = new Bundle();
-        bundle.putParcelableArrayList(BUNDLE_KEY_HOTSEAT,
-                getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
-        bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
-                mLauncher.getWorkspace().getScreenWithId(
-                        Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
-
-        return bundle;
-    }
-
-    private ArrayList<AppTarget> getPinnedAppTargetsInViewGroup(ViewGroup viewGroup) {
-        ArrayList<AppTarget> pinnedApps = new ArrayList<>();
-        for (int i = 0; i < viewGroup.getChildCount(); i++) {
-            View child = viewGroup.getChildAt(i);
-            if (isPinnedIcon(child)) {
-                WorkspaceItemInfo itemInfo = (WorkspaceItemInfo) child.getTag();
-                pinnedApps.add(getAppTargetFromItemInfo(itemInfo));
-            }
+    /**
+     * 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;
         }
-        return pinnedApps;
+        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) {
@@ -328,6 +377,7 @@
                 key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
                         appTarget.getClassName()), appTarget.getUser());
             }
+            componentKeys.add(key);
             predictionLog.append(key.toString());
             predictionLog.append(",rank:");
             predictionLog.append(appTarget.getRank());
@@ -335,13 +385,12 @@
             mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
         }
         predictionLog.append("]");
-        if (Utilities.IS_DEBUG_DEVICE) FileLog.d(TAG, predictionLog.toString());
-        updateDependencies();
-        if (isReady()) {
-            fillGapsWithPrediction();
-        } else if (mHotseatEduController != null) {
-            mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+        if (Utilities.IS_DEBUG_DEVICE) {
+            HotseatFileLog.INSTANCE.get(mLauncher).log(TAG, predictionLog.toString());
         }
+        updateDependencies();
+        fillGapsWithPrediction();
+        mPredictionModel.cachePredictionComponentKeys(componentKeys);
     }
 
     private void updateDependencies() {
@@ -365,8 +414,11 @@
                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
         ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
         icon.pin(workspaceItemInfo);
-        AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
-        notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
+        AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(workspaceItemInfo);
+        if (appTarget != null) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                    AppTargetEvent.ACTION_PIN, workspaceItemInfo));
+        }
     }
 
     private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
@@ -445,10 +497,9 @@
         mIconRemoveAnimators.start();
     }
 
-    private void notifyItemAction(AppTarget target, String location, int action) {
+    private void notifyItemAction(AppTargetEvent event) {
         if (mAppPredictor != null) {
-            mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target,
-                    action).setLaunchLocation(location).build());
+            mAppPredictor.notifyAppTargetEvent(event);
         }
     }
 
@@ -466,36 +517,35 @@
     /**
      * Unpins pinned app when it's converted into a folder
      */
-    public void folderCreatedFromWorkspaceItem(ItemInfo info, FolderInfo folderInfo) {
-        if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-            return;
+    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));
         }
-        AppTarget target = getAppTargetFromItemInfo(info);
-        ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
-        ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
-
-        if (isInHotseat(folderInfo) && !getPinnedAppTargetsInViewGroup(hotseatVG).contains(
-                target)) {
-            notifyItemAction(target, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
-        } else if (isInFirstPage(folderInfo) && !getPinnedAppTargetsInViewGroup(
-                firstScreenVG).contains(target)) {
-            notifyItemAction(target, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+        // 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 info, FolderInfo folderInfo) {
-        if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-            return;
+    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));
         }
-        AppTarget target = getAppTargetFromItemInfo(info);
-        if (isInHotseat(info)) {
-            notifyItemAction(target, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
-        } else if (isInFirstPage(info)) {
-            notifyItemAction(target, APP_LOCATION_WORKSPACE, AppTargetEvent.ACTION_PIN);
+        if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(itemInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
+                    AppTargetEvent.ACTION_PIN, itemInfo));
         }
     }
 
@@ -506,35 +556,25 @@
         }
 
         ItemInfo dragInfo = mDragObject.dragInfo;
-        ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
-        ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
-
-        if (dragInfo instanceof WorkspaceItemInfo
-                && dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                && dragInfo.getTargetComponent() != null) {
-            AppTarget appTarget = getAppTargetFromItemInfo(dragInfo);
-            if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
-                if (!getPinnedAppTargetsInViewGroup(hotseatVG).contains(appTarget)) {
-                    notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
-                }
+        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 (!isInFirstPage(dragInfo) && isInFirstPage(mDragObject.originalDragInfo)) {
-                if (!getPinnedAppTargetsInViewGroup(firstScreenVG).contains(appTarget)) {
-                    notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
-                }
-            }
-            if (isInHotseat(dragInfo) && !isInHotseat(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
-            }
-            if (isInFirstPage(dragInfo) && !isInFirstPage(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, AppTargetEvent.ACTION_PIN);
+            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,
@@ -564,8 +604,10 @@
 
     @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        this.mHotSeatItemsCount = profile.numHotseatIcons;
-        createPredictor();
+        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+            this.mHotSeatItemsCount = profile.numHotseatIcons;
+            createPredictor();
+        }
     }
 
     @Override
@@ -588,6 +630,53 @@
         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) {
@@ -602,39 +691,6 @@
         }
     }
 
-    private void performBetaCheck() {
-        if (isReady()) return;
-        int hotseatItemsCount = mHotseat.getShortcutsAndWidgets().getChildCount();
-
-        // -1 to exclude smart space
-        int workspaceItemCount = mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets().getChildCount() - 1;
-
-        // opt user into the feature without onboarding tip or migration if they don't have any
-        // open spots in their hotseat and have more than maxItems in their hotseat + workspace
-
-        if (hotseatItemsCount == mHotSeatItemsCount && workspaceItemCount + hotseatItemsCount
-                > MAX_ITEMS_FOR_MIGRATION) {
-            mLauncher.getSharedPrefs().edit().putBoolean(HotseatEduController.KEY_HOTSEAT_EDU_SEEN,
-                    true).apply();
-
-            LauncherLogProto.Action action = newAction(LauncherLogProto.Action.Type.TOUCH);
-            LauncherLogProto.Target target = newContainerTarget(LauncherLogProto.ContainerType.TIP);
-            action.touch = LauncherLogProto.Action.Touch.TAP;
-            target.tipType = LauncherLogProto.TipType.HYBRID_HOTSEAT;
-            target.controlType = LauncherLogProto.ControlType.HYBRID_HOTSEAT_CANCELED;
-
-            // temporarily encode details in log target (go/hotseat_migration)
-            target.rank = 2;
-            target.cardinality = MAX_ITEMS_FOR_MIGRATION;
-            target.pageIndex = (workspaceItemCount * 1000) + hotseatItemsCount;
-            LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
-            UserEventDispatcher.newInstance(mLauncher).dispatchUserEvent(event, null);
-
-
-        }
-    }
-
     /**
      * Fill in predicted_rank field based on app prediction.
      * Only applicable when {@link ItemInfo#itemType} is PREDICTED_HOTSEAT
@@ -664,29 +720,4 @@
                 && ((WorkspaceItemInfo) view.getTag()).container
                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
     }
-
-    private static boolean isPinnedIcon(View view) {
-        if (!(view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo)) {
-            return false;
-        }
-        ItemInfo info = (ItemInfo) view.getTag();
-        return info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION && (
-                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION);
-    }
-
-    private static boolean isInHotseat(ItemInfo itemInfo) {
-        return itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-    }
-
-    private static boolean isInFirstPage(ItemInfo itemInfo) {
-        return itemInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
-                && itemInfo.screenId == Workspace.FIRST_SCREEN_ID;
-    }
-
-    private static AppTarget getAppTargetFromItemInfo(ItemInfo info) {
-        if (info.getTargetComponent() == null) return null;
-        ComponentName cn = info.getTargetComponent();
-        return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
-                cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
-    }
 }
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
index 304c77f..597c17b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -19,11 +19,13 @@
 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.DashPathEffect;
 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;
@@ -33,16 +35,19 @@
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
-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;
 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;
 
 /**
@@ -51,13 +56,18 @@
 public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
         LauncherAccessibilityDelegate.AccessibilityActionHandler {
 
-    private static final float RING_EFFECT_RATIO = 0.11f;
+    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 int mNormalizedIconRadius;
+    private final int mNormalizedIconRadius;
+    private final BlurMaskFilter mShadowFilter;
+    private int mPlateColor;
+    boolean mDrawForDrag = false;
 
     public PredictedAppIcon(Context context) {
         this(context, null, 0);
@@ -69,17 +79,20 @@
 
     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+        mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
         mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
-        setOnClickListener(ItemClickHandler.INSTANCE);
-        setOnFocusChangeListener(Launcher.getLauncher(context).getFocusHandler());
+        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) {
-            drawEffect(canvas);
+            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);
         }
@@ -102,7 +115,7 @@
     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
         super.applyFromWorkspaceItem(info);
         int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
-        mIconRingPaint.setColor(ColorUtils.setAlphaComponent(color, 200));
+        mPlateColor = ColorUtils.setAlphaComponent(color, 200);
         if (mIsPinned) {
             setContentDescription(info.contentDescription);
         } else {
@@ -136,9 +149,11 @@
 
     @Override
     public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
-        accessibilityNodeInfo.addAction(
-                new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
-                        getContext().getText(R.string.pin_prediction)));
+        if (!mIsPinned) {
+            accessibilityNodeInfo.addAction(
+                    new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
+                            getContext().getText(R.string.pin_prediction)));
+        }
     }
 
     @Override
@@ -172,9 +187,49 @@
         return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
     }
 
-    private void drawEffect(Canvas canvas) {
-        getShape().drawShape(canvas, getOutlineOffsetX(), getOutlineOffsetY(),
-                mNormalizedIconRadius, mIconRingPaint);
+    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;
+        };
     }
 
     /**
@@ -184,6 +239,8 @@
         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;
     }
 
@@ -203,10 +260,8 @@
             mOffsetX = icon.getOutlineOffsetX();
             mOffsetY = icon.getOutlineOffsetY();
             mIconRadius = icon.mNormalizedIconRadius;
-            mOutlinePaint.setStyle(Paint.Style.STROKE);
-            mOutlinePaint.setStrokeWidth(5);
-            mOutlinePaint.setPathEffect(new DashPathEffect(new float[]{15, 15}, 0));
-            mOutlinePaint.setColor(Color.argb(100, 245, 245, 245));
+            mOutlinePaint.setStyle(Paint.Style.FILL);
+            mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
         }
 
         /**
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
index a02d9c3..9a03d92 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -15,28 +15,47 @@
  */
 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.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.WorkspaceItemInfo;
+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;
@@ -48,16 +67,21 @@
 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.RecentsModel;
 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 {
@@ -68,20 +92,40 @@
      */
     public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
             SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
-    private HotseatPredictionController mHotseatPredictionController;
 
     @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);
-        onStateOrResumeChanged();
+        onStateOrResumeChanging(false /* inTransition */);
     }
 
     @Override
@@ -96,11 +140,9 @@
     @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
-                && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0) {
-            onStateOrResumeChanged();
+                | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+            onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
         }
 
         if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
@@ -136,32 +178,28 @@
     }
 
     /**
-     * Returns Prediction controller for hybrid hotseat
-     */
-    public HotseatPredictionController getHotseatPredictionController() {
-        return mHotseatPredictionController;
-    }
-
-    /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
      */
-    private void onStateOrResumeChanged() {
+    private void onStateOrResumeChanging(boolean inTransition) {
         LauncherState state = getStateManager().getState();
         DeviceProfile profile = getDeviceProfile();
-        boolean visible = (state == NORMAL || state == OVERVIEW) && isUserActive()
-                && !profile.isVerticalBarLayout();
+        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) {
+        if (state == NORMAL && !inTransition) {
             ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
         }
     }
 
     @Override
-    public void finishBindingItems(int pageBoundFirst) {
-        super.finishBindingItems(pageBoundFirst);
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) {
+        super.bindPredictedItems(appInfos, ranks);
         if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.createPredictor();
+            mHotseatPredictionController.showCachedItems(appInfos, ranks);
         }
     }
 
@@ -170,11 +208,60 @@
         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<>();
@@ -182,7 +269,13 @@
         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));
@@ -211,6 +304,11 @@
         return list.toArray(new TouchController[list.size()]);
     }
 
+    @Override
+    public AtomicAnimationFactory createAtomicAnimationFactory() {
+        return new QuickstepAtomicAnimationFactory(this);
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<Launcher> {
 
@@ -220,7 +318,12 @@
 
         @Override
         protected boolean isRecentsInteractive() {
-            return mActivity.isInState(OVERVIEW);
+            return mActivity.isInState(OVERVIEW) || mActivity.isInState(OVERVIEW_MODAL_TASK);
+        }
+
+        @Override
+        protected boolean isRecentsModal() {
+            return mActivity.isInState(OVERVIEW_MODAL_TASK);
         }
 
         @Override
@@ -228,4 +331,13 @@
             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/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 3d6e519..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,19 +15,15 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 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.annotation.TargetApi;
 import android.os.Build;
 import android.util.FloatProperty;
-import android.view.View;
-import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 
@@ -37,6 +33,7 @@
 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;
@@ -60,7 +57,7 @@
             mRecentsView.updateEmptyMessage();
             mRecentsView.resetTaskVisuals();
         }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state, LINEAR);
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
     }
 
@@ -74,26 +71,26 @@
             builder.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
             mRecentsView.updateEmptyMessage();
         } else {
-            builder.getAnim().addListener(
+            builder.addListener(
                     AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
         }
 
-        setAlphas(builder, toState,
-                config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
+        setAlphas(builder, toState);
         builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), LINEAR);
     }
 
-    private void setAlphas(PropertySetter propertySetter, LauncherState state,
-            Interpolator actionInterpolator) {
+    private void setAlphas(PropertySetter propertySetter, LauncherState state) {
         float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 buttonAlpha, LINEAR);
+        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
+                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+    }
 
-        View actionsView = mLauncher.getActionsView();
-        if (actionsView != null) {
-            propertySetter.setFloat(actionsView, VIEW_ALPHA, buttonAlpha, actionInterpolator);
-        }
+    @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 a87d6d1..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
@@ -17,22 +17,20 @@
 
 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);
@@ -43,17 +41,14 @@
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher, false);
-    }
-
-    @Override
     public float getVerticalProgress(Launcher launcher) {
         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() >= recentsView.getTaskViewStartIndex()) {
-            if (recentsView.getCurrentPage() <= taskCount - 1) {
-                dummyTask = recentsView.getCurrentPageTaskView();
-            } else {
-                dummyTask = recentsView.getTaskViewAt(taskCount - 1);
-            }
-        } else {
-            dummyTask = recentsView.getTaskViewAt(0);
-        }
-        return recentsView.getTempAppWindowAnimationHelper().updateForFullscreenOverview(dummyTask)
-                .getScaleAndTranslation();
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return getOverviewScaleAndOffsetForBackgroundState(launcher);
     }
 
     @Override
@@ -97,16 +75,25 @@
     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
-    public float getDepth(Context context) {
+    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 1288e7b..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
@@ -15,42 +15,17 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-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 static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.states.StateAnimationConfig;
 
 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);
-        if (Utilities.isRtl(launcher.getResources())) {
-            result.translationX = -result.translationX;
-        }
-        return result;
-    }
-
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            StateAnimationConfig config) {
-        if (this == 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);
-        }
+    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 bcfb11c..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,41 +15,22 @@
  */
 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.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.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_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.hideShelfInTwoButtonLandscape;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-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.R;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -62,13 +43,11 @@
  */
 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, STATE_FLAGS);
@@ -83,9 +62,9 @@
     }
 
     @Override
-    public int getTransitionDuration(Launcher launcher) {
+    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(launcher).getMode() == NO_BUTTON;
+        boolean isNoButtonMode = SysUINavigationMode.INSTANCE.get(context).getMode() == NO_BUTTON;
         return isNoButtonMode && ENABLE_OVERVIEW_ACTIONS.get() ? 380 : 250;
     }
 
@@ -123,8 +102,8 @@
     }
 
     @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
@@ -138,21 +117,6 @@
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher);
-    }
-
-    @Override
-    public void onStateTransitionEnd(Launcher launcher) {
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
-        DiscoveryBounce.showForOverviewIfNeeded(launcher);
-        RecentsView recentsView = launcher.getOverviewPanel();
-        AccessibilityManagerCompat.sendCustomAccessibilityEvent(
-                recentsView.getPageAt(recentsView.getCurrentPage()),
-                AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
-    }
-
-    @Override
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
         return new PageAlphaProvider(DEACCEL_2) {
             @Override
@@ -164,7 +128,9 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(launcher)) {
+        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;
@@ -205,7 +171,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
@@ -221,35 +187,6 @@
         }
     }
 
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            StateAnimationConfig config) {
-        if ((fromState == NORMAL || fromState == HINT_STATE) && this == OVERVIEW) {
-            if (SysUINavigationMode.getMode(launcher) == 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 = launcher.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_OVERVIEW_SCALE, OVERSHOOT_1_2);
-            Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get()
-                    && removeShelfFromOverview(launcher)
-                    ? 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);
-        }
-    }
-
     public static OverviewState newBackgroundState(int id) {
         return new BackgroundAppState(id);
     }
@@ -261,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 7b4bb02..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,24 +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.GestureState;
-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 LauncherSwipeHandler.
- * @see GestureState.GestureEndTarget#NEW_TASK
+ * @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);
     }
@@ -49,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 8af2747..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,7 +16,6 @@
 
 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;
@@ -31,23 +30,26 @@
 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.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.SystemUiProxy;
@@ -82,7 +84,7 @@
 
     @Override
     protected long getAtomicDuration() {
-        return LauncherAppTransitionManagerImpl.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
+        return QuickstepAtomicAnimationFactory.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
     }
 
     @Override
@@ -178,6 +180,9 @@
 
     @Override
     public boolean onDrag(float displacement, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "FlingAndHoldTouchController");
+        }
         float upDisplacement = -displacement;
         mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
                 || upDisplacement < mMotionPauseMinDisplacement
@@ -206,8 +211,8 @@
             mPeekAnim.cancel();
         }
 
-        Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
-                INDEX_PAUSE_TO_OVERVIEW_ANIM);
+        Animator overviewAnim = mLauncher.createAtomicAnimationFactory()
+                .createStateElementAnimation(INDEX_PAUSE_TO_OVERVIEW_ANIM);
         mAtomicAnim = new AnimatorSet();
         mAtomicAnim.addListener(new AnimationSuccessListener() {
             @Override
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 77118d5..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,17 +15,21 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_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.ValueAnimator;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
@@ -41,7 +45,9 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 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;
@@ -95,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 (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;
@@ -129,14 +154,10 @@
     private void initCurrentAnimation() {
         long accuracy = (long) (getShiftRange() * 2);
         final PendingAnimation builder = new PendingAnimation(accuracy);
-        if (mStartState == OVERVIEW) {
+        if (mStartState.overviewUi) {
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            float pullbackDist = mPullbackDistance;
-            if (!recentsView.isRtl()) {
-                pullbackDist = -pullbackDist;
-            }
-
-            builder.setFloat(recentsView, VIEW_TRANSLATE_X, pullbackDist, PULLBACK_INTERPOLATOR);
+            builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
+                    -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
                 builder.addOnFrameCallback(
                         () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
@@ -227,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
index 71aa2e8..9316938 100644
--- 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
@@ -24,17 +24,22 @@
 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.LauncherStateManager;
 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;
@@ -57,12 +62,20 @@
 
     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
@@ -102,11 +115,31 @@
     @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,
@@ -124,6 +157,7 @@
         if (mCurrentAnimation == null) {
             return;
         }
+        mNormalToHintOverviewScrimAnimator = null;
         mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
             mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
                 mReachedOverview = true;
@@ -139,6 +173,13 @@
         }
     }
 
+    // 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();
@@ -146,6 +187,9 @@
 
     @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);
@@ -165,16 +209,24 @@
     protected void goToOverviewOnDragEnd(float velocity) {
         float velocityDp = dpiFromPx(velocity);
         boolean isFling = Math.abs(velocityDp) > 1;
-        LauncherStateManager stateManager = mLauncher.getStateManager();
-        if (isFling) {
-            // When flinging, go back to home instead of overview.
+        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.start();
+                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();
@@ -183,24 +235,27 @@
                 config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
                 AnimatorSet anim = stateManager.createAtomicAnimation(
                         stateManager.getState(), NORMAL, config);
-                anim.addListener(AnimationSuccessListener.forRunnable(
-                        () -> onSwipeInteractionCompleted(NORMAL, Touch.SWIPE)));
+                anim.addListener(AnimationSuccessListener.forRunnable(() -> {
+                    mIsOverviewRehidden = true;
+                    maybeSwipeInteractionToHomeComplete();
+                }));
                 anim.start();
             }
-        } else {
-            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(this::maybeSwipeInteractionToOverviewComplete);
-            }
+        }
+        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);
         }
     }
 
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 c92a872..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,10 +16,10 @@
 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.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
@@ -28,6 +28,10 @@
 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;
@@ -35,22 +39,21 @@
 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.BaseQuickstepLauncher;
@@ -59,8 +62,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+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;
@@ -117,7 +122,7 @@
         mRecentsView = mLauncher.getOverviewPanel();
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
         mYRange = LayoutUtils.getShelfTrackingDistance(
-            mLauncher, mLauncher.getDeviceProfile());
+            mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
@@ -237,58 +242,34 @@
     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.
-        float startScale = Utilities.mapRange(
-                SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
-                fromScaleAndTranslation.scale,
-                toScaleAndTranslation.scale);
-        fromScaleAndTranslation.translationX = mRecentsView.getOffscreenTranslationX(startScale);
 
         // Set RecentView's initial properties.
-        mRecentsView.setScaleX(fromScaleAndTranslation.scale);
-        mRecentsView.setScaleY(fromScaleAndTranslation.scale);
-        mRecentsView.setTranslationX(fromScaleAndTranslation.translationX);
-        mRecentsView.setTranslationY(fromScaleAndTranslation.translationY);
+        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(mRecentsView, 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(mRecentsView, View.TRANSLATION_Y,
-                toScaleAndTranslation.translationY);
-        Animator scaleAnim = ObjectAnimator.ofFloat(mRecentsView, SCALE_PROPERTY,
-                toScaleAndTranslation.scale);
-        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(mRecentsView, 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();
     }
 
@@ -339,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) {
@@ -453,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);
     }
 
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 9e53959..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
@@ -35,8 +35,6 @@
 
 import android.view.MotionEvent;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
@@ -57,7 +55,7 @@
  */
 public class QuickSwitchTouchController extends AbstractStateChangeTouchController {
 
-    private @Nullable TaskView mTaskToLaunch;
+    protected final RecentsView mOverviewPanel;
 
     public QuickSwitchTouchController(Launcher launcher) {
         this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
@@ -65,6 +63,7 @@
 
     protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         super(l, dir);
+        mOverviewPanel = l.getOverviewPanel();
     }
 
     @Override
@@ -94,7 +93,6 @@
     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);
     }
@@ -102,7 +100,6 @@
     @Override
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
-        mTaskToLaunch = null;
     }
 
     @Override
@@ -141,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 1b3610a..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
@@ -24,6 +24,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.view.MotionEvent;
+import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -57,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;
@@ -74,6 +76,7 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
+        mIsRtl = Utilities.isRtl(activity.getResources());
         SingleAxisSwipeDetector.Direction dir =
             mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
         mDetector = new SingleAxisSwipeDetector(activity, this, dir);
@@ -95,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) {
     }
 
@@ -133,7 +139,7 @@
                     if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
                             .isEventOverView(view, ev)) {
                         // Disable swiping up and down if the task overlay is modal.
-                        if (view.isTaskOverlayModal()) {
+                        if (isRecentsModal()) {
                             mTaskBeingDragged = null;
                             break;
                         }
@@ -197,8 +203,8 @@
         mCurrentAnimationIsGoingUp = goingUp;
         BaseDragLayer dl = mActivity.getDragLayer();
         final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
-        long maxDuration = (long) (2 * secondaryLayerDimension);
-        int verticalFactor = -orientationHandler.getTaskDismissDirectionFactor();
+        long maxDuration = 2 * secondaryLayerDimension;
+        int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
         int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
         if (goingUp) {
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
@@ -209,9 +215,11 @@
             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;
 
@@ -230,7 +238,7 @@
     public void onDragStart(boolean start, float startDisplacement) {
         PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         if (mCurrentAnimation == null) {
-            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement));
+            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
             mDisplacementShift = 0;
         } else {
             mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
@@ -244,7 +252,7 @@
         PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         float totalDisplacement = displacement + mDisplacementShift;
         boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
-                orientationHandler.isGoingUp(totalDisplacement);
+                orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
         if (isGoingUp != mCurrentAnimationIsGoingUp) {
             reInitAnimationController(isGoingUp);
             mFlingBlockCheck.blockFling();
@@ -276,7 +284,7 @@
         float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
         if (fling) {
             logAction = Touch.FLING;
-            boolean goingUp = orientationHandler.isGoingUp(velocity);
+            boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
             goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
         } else {
             logAction = Touch.SWIPE;
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 9dce984..de83caf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -17,53 +17,53 @@
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 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.ObjectAnimator;
-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.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.quickstep.util.AppWindowAnimationHelper;
-import com.android.quickstep.util.AppWindowAnimationHelper.TransformParams;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.quickstep.util.RemoteAnimationProvider;
+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 (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> extends
+final class AppToOverviewAnimationProvider<T extends StatefulActivity<?>> extends
         RemoteAnimationProvider {
 
     private static final long RECENTS_LAUNCH_DURATION = 250;
     private static final String TAG = "AppToOverviewAnimationProvider";
 
-    private final BaseActivityInterface<T> mActivityInterface;
+    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(BaseActivityInterface<T> activityInterface, int targetTaskId) {
+    AppToOverviewAnimationProvider(
+            BaseActivityInterface<?, T> activityInterface, int targetTaskId,
+            RecentsAnimationDeviceState deviceState) {
         mActivityInterface = activityInterface;
         mTargetTaskId = targetTaskId;
+        mDeviceState = deviceState;
     }
 
     /**
@@ -75,16 +75,12 @@
     boolean onActivityReady(T activity, Boolean wasVisible) {
         activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
         AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
-        BaseActivityInterface.AnimationFactory factory =
-                mActivityInterface.prepareRecentsUI(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.createActivityInterface(RECENTS_LAUNCH_DURATION);
         factory.setRecentsAttachedToAppWindow(true, false);
         mActivity = activity;
@@ -101,31 +97,25 @@
     @Override
     public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets) {
-        if (mRecentsView != null) {
-            mRecentsView.setRunningTaskIconScaledDown(true);
+        PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+        if (mActivity == null) {
+            Log.e(TAG, "Animation created, before activity");
+            return pa.buildAnim();
         }
 
-        AnimatorSet anim = new AnimatorSet();
-        anim.addListener(new AnimationSuccessListener() {
+        mRecentsView.setRunningTaskIconScaledDown(true);
+        pa.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
                 mActivityInterface.onSwipeUpToRecentsComplete();
-                if (mRecentsView != null) {
-                    mRecentsView.animateUpRunningTaskIconScale();
-                }
+                mRecentsView.animateUpRunningTaskIconScale();
             }
         });
-        if (mActivity == null) {
-            Log.e(TAG, "Animation created, before activity");
-            return anim;
-        }
 
         DepthController depthController = mActivityInterface.getDepthController();
         if (depthController != null) {
-            anim.play(ObjectAnimator.ofFloat(depthController, DEPTH,
-                    BACKGROUND_APP.getDepth(mActivity),
-                    OVERVIEW.getDepth(mActivity))
-                    .setDuration(RECENTS_LAUNCH_DURATION));
+            pa.addFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(mActivity),
+                    OVERVIEW.getDepth(mActivity), TOUCH_RESPONSE_INTERPOLATOR);
         }
 
         RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets,
@@ -135,53 +125,41 @@
         RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mTargetTaskId);
         if (runningTaskTarget == null) {
             Log.e(TAG, "No closing app");
-            return anim;
+            return pa.buildAnim();
         }
 
-        final AppWindowAnimationHelper clipHelper = new AppWindowAnimationHelper(
-            mRecentsView.getPagedViewOrientedState(), 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();
-        mActivityInterface.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity,
-                targetRect);
-        clipHelper.updateTargetRect(targetRect);
-        clipHelper.prepareAnimation(mActivity.getDeviceProfile(), false /* isOpening */);
+        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()
-                .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())
-                    .setTargetSet(targets)
-                    .setLauncherOnTop(true);
-            clipHelper.applyTransform(params);
-        });
+                .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()) {
-            // If we are animating home, fade in the opening targets
-            RemoteAnimationTargets openingSet = new RemoteAnimationTargets(appTargets,
-                    wallpaperTargets, MODE_OPENING);
+            params.setHomeBuilderProxy((builder, app, p) -> builder.withAlpha(1 - p.getProgress()));
 
-            TransactionCompat transaction = new TransactionCompat();
-            valueAnimator.addUpdateListener((v) -> {
-                for (RemoteAnimationTargetCompat app : openingSet.apps) {
-                    transaction.setAlpha(app.leash, (float) v.getAnimatedValue());
-                }
-                transaction.apply();
-            });
+            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));
         }
-        return anim;
+
+        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 f81c56f..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,57 +15,38 @@
  */
 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.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 android.animation.Animator;
 import android.annotation.TargetApi;
 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.util.Log;
-import android.util.Pair;
 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.model.PagedViewOrientedState;
-import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.PortraitPagedViewHandler;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
+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.AppWindowAnimationHelper;
-import com.android.quickstep.util.AppWindowAnimationHelper.TransformParams;
 import com.android.quickstep.util.RectFSpringAnim;
+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;
@@ -74,39 +55,14 @@
  * 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 RecentsAnimationListener {
+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();
 
-    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
-    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;
-    // 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 final Context mContext;
-    protected final RecentsAnimationDeviceState mDeviceState;
-    protected final GestureState mGestureState;
-    protected final BaseActivityInterface<T> mActivityInterface;
+    protected final BaseActivityInterface<?, T> mActivityInterface;
     protected final InputConsumerController mInputConsumer;
 
-    protected AppWindowAnimationHelper mAppWindowAnimationHelper;
-    protected final TransformParams mTransformParams = new TransformParams();
-
-    // 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 ActivityInitListener mActivityInitListener;
 
     protected RecentsAnimationController mRecentsAnimationController;
@@ -117,31 +73,30 @@
 
     protected T mActivity;
     protected Q mRecentsView;
-    protected DeviceProfile mDp;
-    private final int mPageSpacing;
 
     protected Runnable mGestureEndCallback;
 
     protected MultiStateCallback mStateCallback;
 
     protected boolean mCanceled;
-    protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
-    private PagedViewOrientedState mOrientedState;
+    private boolean mRecentsViewScrollLinked = false;
 
     protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, InputConsumerController inputConsumer) {
-        mContext = context;
-        mDeviceState = deviceState;
-        mGestureState = gestureState;
+        super(context, deviceState, gestureState, new TransformParams());
         mActivityInterface = gestureState.getActivityInterface();
-        mActivityInitListener =
-                mActivityInterface.createActivityInitListener(this::onActivityInit);
+        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumer = inputConsumer;
-        mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
-        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-            .getDeviceProfile(mContext));
+    }
+
+    /**
+     * 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() {
@@ -152,28 +107,6 @@
         return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : 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 > mDragLengthFactorStartPullback) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        mDragLengthFactorStartPullback, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = mDragLengthFactorStartPullback + pullbackProgress
-                        * (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
-            }
-        }
-
-        mCurrentShift.updateValue(shift);
-    }
-
     public void setGestureEndCallback(Runnable gestureEndCallback) {
         mGestureEndCallback = gestureEndCallback;
     }
@@ -181,10 +114,10 @@
     public abstract Intent getLaunchIntent();
 
     protected void linkRecentsViewScroll() {
-        SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
+        SurfaceTransactionApplier.create(mRecentsView, applier -> {
             mTransformParams.setSyncTransactionApplier(applier);
             runOnRecentsAnimationStart(() ->
-                    mRecentsAnimationTargets.addDependentTransactionApplier(applier));
+                    mRecentsAnimationTargets.addReleaseCheck(applier));
         });
 
         mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
@@ -192,45 +125,58 @@
                 updateFinalShift();
             }
         });
-        mRecentsView.setAppWindowAnimationHelper(mAppWindowAnimationHelper);
         runOnRecentsAnimationStart(() ->
                 mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
                         mRecentsAnimationTargets));
+        mRecentsViewScrollLinked = true;
     }
 
-    protected void startNewTask(int successStateFlag, Consumer<Boolean> resultCallback) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_FROM_RECENTS, "startNewTask1");
-        }
+    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.
             mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
                     true /* freezeTaskList */);
         } else {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_START_FROM_RECENTS, "startNewTask2");
-            }
             int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
             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 (success) {
+                                    if (hasTaskPreviouslyAppeared) {
+                                        onRestartPreviouslyAppearedTask();
+                                    }
+                                } else {
                                     mActivityInterface.onLaunchTaskFailed();
                                     nextTask.notifyTaskLaunchFailed(TAG);
-                                } else {
-                                    mActivityInterface.onLaunchTaskSuccess();
+                                    mRecentsAnimationController.finish(true /* toRecents */, null);
                                 }
                             }, MAIN_EXECUTOR.getHandler());
                 }
-                mStateCallback.setStateOnUiThread(successStateFlag);
             }
             mCanceled = false;
         }
-        ActiveGestureLog.INSTANCE.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);
+        }
     }
 
     /**
@@ -246,6 +192,7 @@
     }
 
     /**
+     * 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() {
@@ -257,29 +204,31 @@
             RecentsAnimationTargets targets) {
         mRecentsAnimationController = recentsAnimationController;
         mRecentsAnimationTargets = targets;
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
-        final Rect overviewStackBounds;
+        mTransformParams.setTargetSet(mRecentsAnimationTargets);
         RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
                 mGestureState.getRunningTaskId());
 
-        if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
-            overviewStackBounds = mActivityInterface
-                    .getOverviewWindowBounds(targets.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(targets.homeContentInsets);
-        dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
-            mAppWindowAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+            mTaskViewSimulator.setPreview(runningTaskTarget);
         }
 
-        mAppWindowAnimationHelper.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);
+        }
 
         // Notify when the animation starts
         if (!mRecentsAnimationStartCallbacks.isEmpty()) {
@@ -308,50 +257,37 @@
         }
     }
 
-    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);
+    @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);
+            }
         }
     }
 
-    protected void initTransitionEndpoints(DeviceProfile dp) {
-        mDp = dp;
+    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
+    protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
 
-        mTransitionDragLength = mActivityInterface.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.
-            mAppWindowAnimationHelper.updateHomeBounds(getStackBounds(dp));
-        }
-        int displayRotation = 0;
-        if (mOrientedState != null) {
-            // TODO(b/150300347): The first recents animation after launcher is started with the
-            //  foreground app not in landscape will look funky until that bug is fixed
-            displayRotation = mOrientedState.getDisplayRotation();
-        }
-        RotationHelper.getTargetRectForRotation(TEMP_RECT, dp.widthPx, dp.heightPx,
-            displayRotation);
-        mAppWindowAnimationHelper.updateTargetRect(TEMP_RECT);
-        if (mDeviceState.isFullyGesturalNavMode()) {
-            // We can drag all the way to the top of the screen.
-            // TODO(b/149609070): Landscape apps are currently limited in
-            //   their ability to scale past the target rect.
-            float dragFactor = (float) dp.heightPx / mTransitionDragLength;
-            mDragLengthFactor = displayRotation == 0 ? dragFactor : Math.min(1.0f, dragFactor);
-            Pair<Float, Float> dragFactorStartAndMaxProgress =
-                    mActivityInterface.getSwipeUpPullbackStartAndMaxProgress();
-            mDragLengthFactorStartPullback = dragFactorStartAndMaxProgress.first;
-            mDragLengthFactorMaxPullback = dragFactorStartAndMaxProgress.second;
-        }
+    /**
+     * @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;
     }
 
     /**
@@ -361,12 +297,14 @@
 
     protected boolean onActivityInit(Boolean alreadyOnHome) {
         T createdActivity = mActivityInterface.getCreatedActivity();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
+        }
         if (createdActivity != null) {
-            mOrientedState = ((RecentsView) createdActivity.getOverviewPanel())
-                .getPagedViewOrientedState();
-            mAppWindowAnimationHelper = new AppWindowAnimationHelper(mOrientedState, mContext);
-            initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-                .getDeviceProfile(mContext));
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
+            }
+            initTransitionEndpoints(createdActivity.getDeviceProfile());
         }
         return true;
     }
@@ -389,7 +327,7 @@
     public abstract void onMotionPauseChanged(boolean isPaused);
 
     @UiThread
-    public void onGestureStarted() { }
+    public void onGestureStarted(boolean isLikelyToStartNewTask) { }
 
     @UiThread
     public abstract void onGestureCancelled();
@@ -401,186 +339,47 @@
 
     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
         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 offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
-        float taskSize = getOrientationHandler()
-            .getPrimarySize(mAppWindowAnimationHelper.getTargetRect());
-        float offsetScale = getTaskCurveScaleForOffset(offset, taskSize);
-        mTransformParams.setProgress(shift)
-                .setOffset(offset)
-                .setOffsetScale(offsetScale)
-                .setTargetSet(mRecentsAnimationTargets)
-                .setLauncherOnTop(true);
-        mAppWindowAnimationHelper.applyTransform(mTransformParams);
-    }
-
-    private float getTaskCurveScaleForOffset(float offset, float taskSize) {
-        int dpPixel = getOrientationHandler().getShortEdgeLength(mDp);
-        float distanceToReachEdge = dpPixel / 2 + taskSize / 2 + mPageSpacing;
-        float interpolation = Math.min(1, offset / distanceToReachEdge);
-        return TaskView.getCurveScaleForInterpolation(interpolation);
-    }
-
-    protected PagedOrientationHandler getOrientationHandler() {
-        if (mOrientedState == null) {
-            return new PortraitPagedViewHandler();
+    protected void applyWindowTransform() {
+        if (mWindowTransitionController != null) {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
         }
-        return mOrientedState.getOrientationHandler();
+        if (mRecentsAnimationTargets != null) {
+            if (mRecentsViewScrollLinked) {
+                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+            }
+            mTaskViewSimulator.apply(mTransformParams);
+        }
     }
 
-    /**
-     * 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 RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-        final View floatingView = homeAnimationFactory.getFloatingView();
-        final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
-        final RectF startRect = new RectF(
-            mAppWindowAnimationHelper.applyTransform(
-                mTransformParams.setProgress(startProgress)
-                    .setTargetSet(mRecentsAnimationTargets)
-                    .setLauncherOnTop(false)));
-        if (isFloatingIconView) {
-            RotationHelper.mapInverseRectFromNormalOrientation(startRect,
-                mDp.widthPx, mDp.heightPx, mOrientedState.getDisplayRotation());
+        RectFSpringAnim anim =
+                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
+        if (mRecentsAnimationTargets != null) {
+            mRecentsAnimationTargets.addReleaseCheck(anim);
         }
-        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
-        if (isFloatingIconView) {
-            FloatingIconView fiv = (FloatingIconView) floatingView;
-            anim.addAnimatorListener(fiv);
-            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
-        }
-
-        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 = mAppWindowAnimationHelper.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;
-        final RectF rotatedRect = new RectF();
-        anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() {
-
-            @Override
-            public void onUpdate(RectF currentRect, float progress) {
-                homeAnim.setPlayFraction(progress);
-
-                rotatedRect.set(currentRect);
-                if (isFloatingIconView) {
-                    RotationHelper.mapRectFromNormalOrientation(rotatedRect,
-                        mDp.widthPx, mDp.heightPx, mOrientedState.getDisplayRotation());
-                    mTransformParams.setCornerRadius(endRadius * progress + startRadius
-                        * (1f - progress));
-                }
-                mTransformParams.setProgress(
-                    Utilities.mapRange(progress, startTransformProgress, endTransformProgress))
-                    .setCurrentRect(rotatedRect)
-                    .setTargetAlpha(getWindowAlpha(progress));
-                mAppWindowAnimationHelper.applyTransform(mTransformParams);
-
-                if (isFloatingIconView) {
-                    ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
-                            windowAlphaThreshold, mAppWindowAnimationHelper.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;
     }
 
-    /**
-     * @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);
-    }
-
     public interface Factory {
 
-        BaseSwipeUpHandler newHandler(GestureState gestureState, 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/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
similarity index 85%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
index 4cfa6f1..37aa0da 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -22,6 +22,12 @@
 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;
@@ -29,8 +35,8 @@
 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.GestureState.STATE_TASK_APPEARED_DURING_SWITCH;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
@@ -38,46 +44,41 @@
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
-import android.animation.AnimatorSet;
 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.graphics.RectF;
 import android.os.Build;
 import android.os.SystemClock;
-import android.util.Log;
 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.StatsLogManager;
 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.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.BaseActivityInterface.HomeAnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
@@ -85,17 +86,20 @@
 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.TaskStackChangeListener;
 
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
+ * TODO: Merge this with BaseSwipeUpHandler
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherSwipeHandler<T extends BaseDraggingActivity>
-        extends BaseSwipeUpHandler<T, RecentsView> implements OnApplyWindowInsetsListener {
-    private static final String TAG = LauncherSwipeHandler.class.getSimpleName();
+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;
 
@@ -107,9 +111,11 @@
     }
 
     // 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");
+    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 =
@@ -121,7 +127,7 @@
     private static final int STATE_SCALED_CONTROLLER_RECENTS =
             getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
 
-    private static final int STATE_HANDLER_INVALIDATED =
+    protected static final int STATE_HANDLER_INVALIDATED =
             getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
     private static final int STATE_GESTURE_STARTED =
             getFlagForIndex(7, "STATE_GESTURE_STARTED");
@@ -132,7 +138,7 @@
 
     private static final int STATE_CAPTURE_SCREENSHOT =
             getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
-    private static final int STATE_SCREENSHOT_CAPTURED =
+    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");
@@ -148,7 +154,6 @@
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
 
     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;
@@ -163,7 +168,7 @@
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final TaskAnimationManager mTaskAnimationManager;
+    protected final TaskAnimationManager mTaskAnimationManager;
 
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
@@ -193,7 +198,7 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public LauncherSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
@@ -201,6 +206,8 @@
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
+
+        initAfterSubclassConstructor();
         initStateCallbacks();
     }
 
@@ -220,9 +227,6 @@
                         | STATE_GESTURE_CANCELLED,
                 this::resetStateForAnimationCancel);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
-                this::sendRemoteAnimationsToAnimationFactory);
-
         mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
         mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
@@ -254,14 +258,16 @@
                         | STATE_RECENTS_SCROLLING_FINISHED,
                 this::onSettledOnEndTarget);
 
-        mGestureState.runOnceAtState(STATE_TASK_APPEARED_DURING_SWITCH, this::onTaskAppeared);
-
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
         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,
@@ -276,6 +282,7 @@
         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;
@@ -294,7 +301,6 @@
 
         mRecentsView = activity.getOverviewPanel();
         mRecentsView.setOnPageTransitionEndCallback(null);
-        linkRecentsViewScroll();
         addLiveTileOverlay();
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
@@ -311,6 +317,8 @@
             // so we need to kick off switching to the overview predictions as soon as possible
             mActivityInterface.updateOverviewPredictionState();
         }
+        linkRecentsViewScroll();
+
         return true;
     }
 
@@ -327,14 +335,14 @@
         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(
-                        mWasLauncherAlreadyVisible, true,
-                        this::onAnimatorPlaybackControllerCreated);
+                mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState,
+                        mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
                 maybeUpdateRecentsAttachedState(false /* animate */);
             };
             if (mWasLauncherAlreadyVisible) {
@@ -409,6 +417,10 @@
             updateSysUiFlags(mCurrentShift.value);
             return;
         }
+        notifyGestureAnimationStartToRecents();
+    }
+
+    protected void notifyGestureAnimationStartToRecents() {
         mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
     }
 
@@ -416,10 +428,6 @@
         mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
     }
 
-    private void sendRemoteAnimationsToAnimationFactory() {
-        mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationTargets);
-    }
-
     private void initializeLauncherAnimationController() {
         buildAnimationController();
 
@@ -473,32 +481,24 @@
         } 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) {
+        setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
+    }
+
+    private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
         if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
             mIsLikelyToStartNewTask = isLikelyToStartNewTask;
-            maybeUpdateRecentsAttachedState();
+            maybeUpdateRecentsAttachedState(animate);
         }
     }
 
@@ -542,7 +542,6 @@
     private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
         mLauncherTransitionController = anim;
         mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mAnimationFactory.adjustActivityControllerInterpolators();
         mLauncherTransitionController.dispatchOnStart();
         updateLauncherTransitionProgress();
     }
@@ -554,16 +553,11 @@
 
     @Override
     public void updateFinalShift() {
-        if (mRecentsAnimationTargets != null) {
-            applyTransformUnchecked();
-            updateSysUiFlags(mCurrentShift.value);
-        }
-
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mRecentsAnimationTargets != null) {
                 LiveTileOverlay.INSTANCE.update(
-                        mAppWindowAnimationHelper.getCurrentRectWithInsets(),
-                        mAppWindowAnimationHelper.getCurrentCornerRadius());
+                        mTaskViewSimulator.getCurrentCropRect(),
+                        mTaskViewSimulator.getCurrentCornerRadius());
             }
         }
 
@@ -575,6 +569,8 @@
             }
         }
 
+        updateSysUiFlags(mCurrentShift.value);
+        applyWindowTransform();
         updateLauncherTransitionProgress();
     }
 
@@ -593,17 +589,20 @@
      * @param windowProgress 0 == app, 1 == overview
      */
     private void updateSysUiFlags(float windowProgress) {
-        if (mRecentsView != null) {
+        if (mRecentsAnimationController != null && mRecentsView != null) {
+            TaskView runningTask = mRecentsView.getRunningTaskView();
             TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
             int centermostTaskFlags = centermostTask == null ? 0
                     : centermostTask.getThumbnail().getSysUiStatusNavFlags();
-            boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
+            boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
+            boolean quickswitchThresholdPassed = centermostTask != runningTask;
+
             // We will handle the sysui flags based on the centermost task view.
-            if (mRecentsAnimationController != null) {
-                mRecentsAnimationController.setWindowThresholdCrossed(centermostTaskFlags != 0
-                        && useHomeScreenFlags);
-            }
-            int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags;
+            mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
+                    ||  (quickswitchThresholdPassed && centermostTaskFlags != 0));
+            mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed);
+
+            int sysuiFlags = swipeUpThresholdPassed ? 0 : centermostTaskFlags;
             mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
         }
     }
@@ -633,8 +632,9 @@
     }
 
     @Override
-    public void onGestureStarted() {
+    public void onGestureStarted(boolean isLikelyToStartNewTask) {
         notifyGestureStartedAsync();
+        setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
         mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
     }
@@ -690,12 +690,8 @@
     protected InputConsumer createNewInputProxyHandler() {
         endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
         endLauncherTransitionController();
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            // Hide the task view, if not already hidden
-            setTargetAlphaProvider(LauncherSwipeHandler::getHiddenTargetAlpha);
-        }
 
-        BaseDraggingActivity activity = mActivityInterface.getCreatedActivity();
+        StatefulActivity activity = mActivityInterface.getCreatedActivity();
         return activity == null ? InputConsumer.NO_OP
                 : new OverviewInputConsumer(mGestureState, activity, null, true);
     }
@@ -728,22 +724,20 @@
                 mStateCallback.setState(STATE_RESUME_LAST_TASK);
                 break;
         }
+        ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + mGestureState.getEndTarget());
     }
 
-    private void onTaskAppeared() {
-        RemoteAnimationTargetCompat app = mGestureState.getAnimationTarget();
-        if (mRecentsAnimationController != null && app != null) {
-
-            // TODO(b/152480470): Update Task target animation after onTaskAppeared holistically.
-            /* android.util.Log.d("LauncherSwipeHandler", "onTaskAppeared");
-
-            final boolean result = mRecentsAnimationController.removeTaskTarget(app);
-            mGestureState.setAnimationTarget(null);
-            android.util.Log.d("LauncherSwipeHandler", "removeTask, result=" + result); */
-
-            mRecentsAnimationController.finish(false /* toRecents */,
-                    null /* onFinishComplete */);
+    @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,
@@ -801,12 +795,6 @@
             }
         }
 
-        if (endTarget == RECENTS || endTarget == HOME) {
-            // Since we're now done quickStepping, we want to only listen for touch events
-            // for the main orientation's nav bar, instead of multiple
-            mDeviceState.enableMultipleRegions(false);
-        }
-
         if (mDeviceState.isOverviewDisabled() && (endTarget == RECENTS || endTarget == LAST_TASK)) {
             return LAST_TASK;
         }
@@ -915,6 +903,27 @@
                 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. */
@@ -925,37 +934,39 @@
                 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()) {
+                // 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 (mGestureState.getEndTarget() == HOME) {
-            HomeAnimationFactory homeAnimFactory;
-            if (mActivity != null) {
-                homeAnimFactory = mActivityInterface.prepareHomeUI();
-            } else {
-                homeAnimFactory = new HomeAnimationFactory() {
-                    @NonNull
-                    @Override
-                    public RectF getWindowTargetRect() {
-                        RectF fallbackTarget = new RectF(mAppWindowAnimationHelper.getTargetRect());
-                        Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f);
-                        return fallbackTarget;
-                    }
+        // 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);
+        }
 
-                    @NonNull
-                    @Override
-                    public AnimatorPlaybackController createActivityAnimationToHome() {
-                        return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
-                    }
-                };
-                mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
-                        isPresent -> mRecentsView.startHome());
-            }
+        if (mGestureState.getEndTarget() == HOME) {
+            HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
             RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
             windowAnim.addAnimatorListener(new AnimationSuccessListener() {
                 @Override
@@ -984,21 +995,30 @@
             windowAnim.addListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
-                    if (TestProtocol.sDebugTracing) {
-                        Log.d(TestProtocol.NO_START_FROM_RECENTS, "onAnimationSuccess");
-                    }
                     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 (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.
-                        mGestureState.setEndTarget(LAST_TASK);
+                    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);
                 }
@@ -1023,7 +1043,6 @@
             mLauncherTransitionController.dispatchSetInterpolator(t -> end);
         } else {
             mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-            mAnimationFactory.adjustActivityControllerInterpolators();
         }
         mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
 
@@ -1043,9 +1062,11 @@
     }
 
     private void continueComputingRecentsScrollIfNecessary() {
-        if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)) {
+        if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
+                && !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)
+                && !mCanceled) {
             computeRecentsScrollIfInvisible();
-            mRecentsView.post(this::continueComputingRecentsScrollIfNecessary);
+            mRecentsView.postOnAnimation(this::continueComputingRecentsScrollIfNecessary);
         }
     }
 
@@ -1077,7 +1098,7 @@
                 }
                 // Make sure recents is in its final state
                 maybeUpdateRecentsAttachedState(false);
-                mActivityInterface.onSwipeUpToHomeComplete();
+                mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
             }
         });
         return anim;
@@ -1121,8 +1142,9 @@
 
     @UiThread
     private void startNewTaskInternal() {
-        startNewTask(STATE_HANDLER_INVALIDATED, success -> {
+        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();
@@ -1132,6 +1154,12 @@
         });
     }
 
+    @Override
+    protected void onRestartPreviouslyAppearedTask() {
+        super.onRestartPreviouslyAppearedTask();
+        reset();
+    }
+
     private void reset() {
         mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
@@ -1147,19 +1175,6 @@
                 .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);
-            mGestureState.setFinishingRecentsAnimationTaskId(newRunningTaskId);
-        }
     }
 
     private void invalidateHandler() {
@@ -1170,6 +1185,7 @@
         }
 
         mActivityInitListener.unregister();
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
         mTaskSnapshot = null;
     }
 
@@ -1209,10 +1225,7 @@
         mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
     }
 
-    private void switchToScreenshot() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_FROM_RECENTS, "switchToScreenshot");
-        }
+    protected void switchToScreenshot() {
         final int runningTaskId = mGestureState.getRunningTaskId();
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mRecentsAnimationController != null) {
@@ -1278,14 +1291,15 @@
             // 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),
-                    true /* sendUserLeaveHint */);
+            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();
@@ -1300,11 +1314,6 @@
         reset();
     }
 
-    private void setTargetAlphaProvider(TargetAlphaProvider provider) {
-        mAppWindowAnimationHelper.setTaskAlphaCallback(provider);
-        updateFinalShift();
-    }
-
     private void addLiveTileOverlay() {
         if (LiveTileOverlay.INSTANCE.attach(mActivity.getRootView().getOverlay())) {
             mRecentsView.setLiveTileOverlayAttached(true);
@@ -1316,13 +1325,6 @@
         mRecentsView.setLiveTileOverlayAttached(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/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 4abb86a..33b9cde 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -15,30 +15,23 @@
  */
 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 static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 
-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 android.view.MotionEvent;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.touch.PortraitPagedViewHandler;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -50,19 +43,20 @@
  * currently running one and apps should interact with the {@link RecentsActivity} as opposed
  * to the in-launcher one.
  */
-public final class FallbackActivityInterface implements
-        BaseActivityInterface<RecentsActivity> {
+public final class FallbackActivityInterface extends
+        BaseActivityInterface<RecentsState, RecentsActivity> {
 
-    public FallbackActivityInterface() { }
+    public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
 
-    @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        // TODO:
+    private FallbackActivityInterface() {
+        super(false, DEFAULT, BACKGROUND_APP);
     }
 
+    /** 2 */
     @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        LayoutUtils.calculateFallbackTaskSize(context, dp, outRect);
+    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();
@@ -73,17 +67,13 @@
         }
     }
 
+    /** 4 */
     @Override
-    public void onSwipeUpToRecentsComplete() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return;
-        }
-        RecentsView recentsView = activity.getOverviewPanel();
-        recentsView.getClearAllButton().setVisibilityAlpha(1);
-        recentsView.setDisallowScrollToClearAll(false);
+    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
+        onSwipeUpToRecentsComplete();
     }
 
+    /** 5 */
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
         // This class becomes active when the screen is locked.
@@ -91,95 +81,13 @@
         // set to zero prior to this class becoming active.
     }
 
-    @NonNull
+    /** 6 */
     @Override
-    public HomeAnimationFactory prepareHomeUI() {
-        RecentsActivity activity = getCreatedActivity();
-        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(boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        RecentsActivity activity = getCreatedActivity();
-        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(RemoteAnimationTargets targets) {
-                isAnimatingToRecents = targets != null && targets.isAnimatingHome();
-                if (!isAnimatingToRecents) {
-                    rv.setContentAlpha(1);
-                }
-                createActivityInterface(getSwipeUpDestinationAndLength(
-                        activity.getDeviceProfile(), activity, new Rect()));
-            }
-
-            @Override
-            public void createActivityInterface(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);
-            }
-        };
+    public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+        DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
+        factory.initUI();
+        return factory;
     }
 
     @Override
@@ -192,7 +100,7 @@
     @Nullable
     @Override
     public RecentsActivity getCreatedActivity() {
-        return BaseRecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+        return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
     }
 
     @Nullable
@@ -217,12 +125,26 @@
     }
 
     @Override
-    public boolean shouldMinimizeSplitScreen() {
+    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();
@@ -247,11 +169,10 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return;
-        }
-        activity.onTaskLaunched();
+    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
index da73bc0..fc7a119 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,515 +15,208 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-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.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.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 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 android.util.ArrayMap;
-import android.view.MotionEvent;
+import android.graphics.Matrix;
 
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.touch.LandscapePagedViewHandler;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.PortraitPagedViewHandler;
-import com.android.launcher3.util.ObjectWrapper;
-import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
-import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
+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.ActivityOptionsCompat;
 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 BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+public class FallbackSwipeHandler extends
+        BaseSwipeUpHandlerV2<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 static class EndTargetAnimationParams {
-        private final float mEndProgress;
-        private final long mDurationMultiplier;
-        private final float mLauncherAlpha;
-
-        EndTargetAnimationParams(float endProgress, long durationMultiplier, float launcherAlpha) {
-            mEndProgress = endProgress;
-            mDurationMultiplier = durationMultiplier;
-            mLauncherAlpha = launcherAlpha;
-        }
-    }
-    private final ArrayMap<GestureEndTarget, EndTargetAnimationParams>
-            mEndTargetAnimationParams = new ArrayMap();
-
-    private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
-
-    private boolean mOverviewThresholdPassed = false;
-
-    private final boolean mInQuickSwitchMode;
-    private final boolean mContinuingLastGesture;
+    private FallbackHomeAnimationFactory mActiveAnimationFactory;
     private final boolean mRunningOverHome;
-    private final boolean mSwipeUpOverHome;
-    private boolean mTouchedHomeDuringTransition;
 
-    private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
-    private RunningWindowAnim mFinishAnimation;
+    private final Matrix mTmpMatrix = new Matrix();
+    private float mMaxLauncherScale = 1;
 
     public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer,
-            boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, deviceState, gestureState, inputConsumer);
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer);
 
-        mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
-        mContinuingLastGesture = continuingLastGesture;
         mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
-        mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
-
-        // Keep the home launcher invisible until we decide to land there.
-        mLauncherAlpha.value = mRunningOverHome ? 1 : 0;
-        if (mSwipeUpOverHome) {
-            mAppWindowAnimationHelper.setBaseAlphaCallback((t, a) -> 1 - mLauncherAlpha.value);
-        } else {
-            mAppWindowAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
-        }
-
-        // Going home has an extra long progress to ensure that it animates into the screen
-        mEndTargetAnimationParams.put(HOME, new EndTargetAnimationParams(3, 100, 1));
-        mEndTargetAnimationParams.put(RECENTS, new EndTargetAnimationParams(1, 300, 0));
-        mEndTargetAnimationParams.put(LAST_TASK, new EndTargetAnimationParams(0, 150, 1));
-        mEndTargetAnimationParams.put(NEW_TASK, new EndTargetAnimationParams(0, 150, 1));
-
-        initStateCallbacks();
-    }
-
-    private void initStateCallbacks() {
-        mStateCallback = new MultiStateCallback(STATE_NAMES);
-
-        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidated);
-        mStateCallback.runOnceAtState(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidatedWithRecents);
-
-        mStateCallback.runOnceAtState(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
-                this::finishAnimationTargetSetAnimationComplete);
-
-        if (mInQuickSwitchMode) {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
-                            | STATE_RECENTS_PRESENT,
-                    this::finishAnimationTargetSet);
-        } else {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
-                    this::finishAnimationTargetSet);
-        }
-    }
-
-    private void onLauncherAlphaChanged() {
-        if (mRecentsAnimationTargets != null && mGestureState.getEndTarget() == null) {
-            applyTransformUnchecked();
-        }
-    }
-
-    @Override
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        super.onActivityInit(alreadyOnHome);
-        mActivity = mActivityInterface.getCreatedActivity();
-        mRecentsView = mActivity.getOverviewPanel();
-        mRecentsView.setOnPageTransitionEndCallback(null);
-        linkRecentsViewScroll();
-        mRecentsView.setDisallowScrollToClearAll(true);
-        mRecentsView.getClearAllButton().setVisibilityAlpha(0);
-        mRecentsView.setZoomProgress(1);
-
-        if (!mContinuingLastGesture) {
-            if (mRunningOverHome) {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
-            } else {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
-            }
-        }
-        mStateCallback.setStateOnUiThread(STATE_RECENTS_PRESENT);
-        mDeviceState.enableMultipleRegions(false);
-        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 new InputConsumer() {
-            @Override
-            public int getType() {
-                return InputConsumer.TYPE_NO_OP;
-            }
-
-            @Override
-            public void onMotionEvent(MotionEvent ev) {
-                mTouchedHomeDuringTransition = true;
-            }
-        };
-    }
-
-    @Override
-    public void onMotionPauseChanged(boolean isPaused) {
-        if (!mInQuickSwitchMode && mDeviceState.isFullyGesturalNavMode()) {
-            updateOverviewThresholdPassed(isPaused);
-        }
-    }
-
-    private void updateOverviewThresholdPassed(boolean passed) {
-        if (passed != mOverviewThresholdPassed) {
-            mOverviewThresholdPassed = passed;
-            if (mSwipeUpOverHome) {
-                mLauncherAlpha.animateToValue(mLauncherAlpha.value, passed ? 0 : 1)
-                        .setDuration(150).start();
-            }
-            performHapticFeedback();
-        }
-    }
-
-    @Override
-    public Intent getLaunchIntent() {
-        if (mInQuickSwitchMode || mSwipeUpOverHome || !mDeviceState.isFullyGesturalNavMode()) {
-            return mGestureState.getOverviewIntent();
-        } else {
-            return mGestureState.getHomeIntent();
-        }
-    }
-
-    @Override
-    public void updateFinalShift() {
-        mTransformParams.setProgress(mCurrentShift.value);
-        if (mRecentsAnimationController != null) {
-            mRecentsAnimationController.setWindowThresholdCrossed(!mInQuickSwitchMode
-                    && (mCurrentShift.value > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD));
-        }
-
-        if (!mInQuickSwitchMode && !mDeviceState.isFullyGesturalNavMode()) {
-            updateOverviewThresholdPassed(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW);
-        }
-
-        if (mRecentsAnimationTargets != null) {
-            applyTransformUnchecked();
-        }
-    }
-
-    @Override
-    public void onGestureCancelled() {
-        updateDisplacement(0);
-        mGestureState.setEndTarget(LAST_TASK);
-        mStateCallback.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
-            mGestureState.setEndTarget(LAST_TASK);
-        } else {
-            float flingThreshold = mContext.getResources()
-                    .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-            boolean isFling = Math.abs(endVelocity) > flingThreshold;
-
-            if (mDeviceState.isFullyGesturalNavMode()) {
-                if (isFling) {
-                    mGestureState.setEndTarget(endVelocity < 0 ? HOME : LAST_TASK);
-                } else if (mOverviewThresholdPassed) {
-                    mGestureState.setEndTarget(RECENTS);
-                } else {
-                    mGestureState.setEndTarget(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW
-                            ? HOME
-                            : LAST_TASK);
-                }
-            } else {
-                GestureEndTarget startState = mSwipeUpOverHome ? HOME : LAST_TASK;
-                if (isFling) {
-                    mGestureState.setEndTarget(endVelocity < 0 ? RECENTS : startState);
-                } else {
-                    mGestureState.setEndTarget(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW
-                            ? RECENTS
-                            : startState);
-                }
-            }
-        }
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
-    }
-
-    @Override
-    public void onConsumerAboutToBeSwitched() {
-        if (mInQuickSwitchMode && mGestureState.getEndTarget() != null) {
-            mGestureState.setEndTarget(NEW_TASK);
-
-            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);
-                    mGestureState.setFinishingRecentsAnimationTaskId(newRunningTaskId);
-                }
-                mRecentsView.setOnScrollChangeListener(null);
-            }
-        } else {
-            mStateCallback.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 (mGestureState.getEndTarget()) {
-            case HOME: {
-                if (mSwipeUpOverHome) {
-                    mRecentsAnimationController.finish(false, null, false);
-                    // Send a home intent to clear the task stack
-                    mContext.startActivity(mGestureState.getHomeIntent());
-                } else {
-                    // Notify swipe-to-home (recents animation) is finished
-                    SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
-                    mRecentsAnimationController.finish(true, () -> {
-                        if (!mTouchedHomeDuringTransition) {
-                            // If the user hasn't interacted with the screen during the transition,
-                            // send a home intent so launcher can go to the default home screen.
-                            // (If they are trying to touch something, we don't want to interfere.)
-                            mContext.startActivity(mGestureState.getHomeIntent());
-                        }
-                    }, true);
-                }
-                break;
-            }
-            case LAST_TASK:
-                mRecentsAnimationController.finish(false, null, false);
-                break;
-            case RECENTS: {
-                if (mSwipeUpOverHome || !mDeviceState.isFullyGesturalNavMode()) {
-                    mRecentsAnimationController.finish(true, null, true);
-                    break;
-                }
-
-                final int runningTaskId = mGestureState.getRunningTaskId();
-                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(runningTaskId);
-                mRecentsAnimationController.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, runningTaskId);
-
-                Intent intent = new Intent(mGestureState.getOverviewIntent())
-                        .putExtras(extras);
-                mContext.startActivity(intent, options.toBundle());
-                mRecentsAnimationController.cleanupScreenshot();
-                break;
-            }
-            case NEW_TASK: {
-                startNewTask(STATE_HANDLER_INVALIDATED, b -> {});
-                break;
-            }
-        }
-
-        mStateCallback.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 || !hasTargets()) {
-                mGestureState.setEndTarget(LAST_TASK);
-            } else {
-                final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
-                final int taskToLaunch = mRecentsView.getNextPage();
-                mGestureState.setEndTarget(
-                        (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
-                                ? NEW_TASK
-                                : LAST_TASK);
-            }
-        }
-
-        EndTargetAnimationParams params = mEndTargetAnimationParams.get(mGestureState.getEndTarget());
-        float endProgress = params.mEndProgress;
-        long duration = (long) (params.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) {
-                    if (mRecentsView != null) {
-                        mRecentsView.setOnPageTransitionEndCallback(FallbackSwipeHandler.this
-                                ::finishAnimationTargetSetAnimationComplete);
-                    } else {
-                        finishAnimationTargetSetAnimationComplete();
-                    }
-                    mFinishAnimation = null;
-                }
-            };
-
-            if (mGestureState.getEndTarget() == HOME && !mRunningOverHome) {
-                mRecentsAnimationController.enableInputProxy(mInputConsumer,
-                        this::createNewInputProxyHandler);
-                RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
-                anim.addAnimatorListener(endListener);
-                anim.start(mContext, mEndVelocityPxPerMs);
-                mFinishAnimation = RunningWindowAnim.wrap(anim);
-            } else {
-
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(mLauncherAlpha.animateToValue(
-                        mLauncherAlpha.value, params.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(RecentsAnimationController controller,
-            RecentsAnimationTargets targets) {
-        super.onRecentsAnimationStart(controller, targets);
-        mRecentsAnimationController.enableInputConsumer();
-
         if (mRunningOverHome) {
-            mAppWindowAnimationHelper.prepareAnimation(mDp, true);
+            mTransformParams.setHomeBuilderProxy(this::updateHomeActivityTransformDuringSwipeUp);
         }
-        applyTransformUnchecked();
-
-        mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
     }
 
     @Override
-    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-
-        // Defer clearing the controller and the targets until after we've updated the state
-        super.onRecentsAnimationCanceled(thumbnailData);
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        if (mRunningOverHome) {
+            mMaxLauncherScale = 1 / mTaskViewSimulator.getFullScreenScale();
+        }
     }
 
-    /**
-     * 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() {
-                PagedOrientationHandler orientationHandler = mRecentsView != null
-                        ? mRecentsView.getPagedOrientationHandler()
-                        : (mDp.isLandscape
-                                ? new LandscapePagedViewHandler()
-                                : new PortraitPagedViewHandler());
-                return HomeAnimationFactory
-                    .getDefaultWindowTargetRect(orientationHandler, mDp);
-            }
+    private void updateHomeActivityTransformDuringSwipeUp(SurfaceParams.Builder builder,
+            RemoteAnimationTargetCompat app, TransformParams params) {
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+                Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
+    }
 
-            @Override
-            public AnimatorPlaybackController createActivityAnimationToHome() {
-                AnimatorSet anim = new AnimatorSet();
-                Animator fadeInLauncher = mLauncherAlpha.animateToValue(mLauncherAlpha.value, 1);
-                fadeInLauncher.setInterpolator(ACCEL_2);
-                anim.play(fadeInLauncher);
-                anim.setDuration(duration);
-                return AnimatorPlaybackController.wrap(anim, duration);
-            }
-        };
-        return createWindowAnimationToHome(startProgress, factory);
+    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 float getWindowAlpha(float progress) {
-        return 1 - ACCEL_1_5.getInterpolation(progress);
+    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
index 33fe5a9..ba8ba33 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
@@ -17,6 +17,7 @@
 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;
@@ -33,6 +34,7 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.quickstep.util.ImageActionUtils;
+import com.android.systemui.shared.recents.model.Task;
 
 import java.util.function.Supplier;
 
@@ -42,9 +44,10 @@
 public class ImageActionsApi {
 
     private static final String TAG = BuildConfig.APPLICATION_ID + "ImageActionsApi";
-    private final Context mContext;
-    private final Supplier<Bitmap> mBitmapSupplier;
-    private final SystemUiProxy mSystemUiProxy;
+
+    protected final Context mContext;
+    protected final Supplier<Bitmap> mBitmapSupplier;
+    protected final SystemUiProxy mSystemUiProxy;
 
     public ImageActionsApi(Context context, Supplier<Bitmap> bitmapSupplier) {
         mContext = context;
@@ -65,7 +68,9 @@
 
         UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
                 mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
-                    intentForUri.putExtra(EXTRA_STREAM, uri);
+                    intentForUri
+                            .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+                            .putExtra(EXTRA_STREAM, uri);
                     return new Intent[]{intentForUri};
                 }, TAG));
 
@@ -84,11 +89,11 @@
      * @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 taskId           of the task that the screenshot was taken of.
+     * @param task             of the task that the screenshot was taken of.
      */
     public void saveScreenshot(Bitmap screenshot, Rect screenshotBounds,
-            Insets visibleInsets, int taskId) {
+            Insets visibleInsets, Task.TaskKey task) {
         ImageActionUtils.saveScreenshot(mSystemUiProxy, screenshot, screenshotBounds, visibleInsets,
-                taskId);
+                task);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 455ae76..edefbe1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -15,35 +15,20 @@
  */
 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.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
+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.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.UserHandle;
-import android.util.Pair;
-import android.view.MotionEvent;
-import android.view.View;
+import android.util.Log;
 import android.view.animation.Interpolator;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -52,25 +37,23 @@
 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.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
 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.plugins.shared.LauncherOverlayManager;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.Consumer;
@@ -79,52 +62,41 @@
 /**
  * {@link BaseActivityInterface} for the in-launcher recents.
  */
-public final class LauncherActivityInterface implements BaseActivityInterface<Launcher> {
+public final class LauncherActivityInterface extends
+        BaseActivityInterface<LauncherState, BaseQuickstepLauncher> {
 
-    private Runnable mAdjustInterpolatorsRunnable;
-    private Pair<Float, Float> mSwipeUpPullbackStartAndMaxProgress =
-            BaseActivityInterface.super.getSwipeUpPullbackStartAndMaxProgress();
+    public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
+
+    private LauncherActivityInterface() {
+        super(true, OVERVIEW, BACKGROUND_APP);
+    }
 
     @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        LayoutUtils.calculateLauncherTaskSize(context, dp, outRect);
+    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);
+            return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler);
         }
     }
 
     @Override
-    public Pair<Float, Float> getSwipeUpPullbackStartAndMaxProgress() {
-        return mSwipeUpPullbackStartAndMaxProgress;
-    }
-
-    @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        LauncherState startState = launcher.getStateManager().getRestState();
-        launcher.getStateManager().goToState(startState, activityVisible);
-    }
-
-    @Override
     public void onSwipeUpToRecentsComplete() {
-        // Re apply state in case we did something funky during the transition.
+        super.onSwipeUpToRecentsComplete();
         Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
+        if (launcher != null) {
+            RecentsView recentsView = launcher.getOverviewPanel();
+            DiscoveryBounce.showForOverviewIfNeeded(launcher,
+                    recentsView.getPagedOrientationHandler());
         }
-        launcher.getStateManager().reapplyState();
-        DiscoveryBounce.showForOverviewIfNeeded(launcher);
     }
 
     @Override
-    public void onSwipeUpToHomeComplete() {
+    public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
         Launcher launcher = getCreatedActivity();
         if (launcher == null) {
             return;
@@ -132,15 +104,8 @@
         // 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();
-        setLauncherHideBackArrow(false);
-    }
-
-    private void setLauncherHideBackArrow(boolean hideBackArrow) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.getRootView().setForceHideBackArrow(hideBackArrow);
+        launcher.getRootView().setForceHideBackArrow(false);
+        notifyRecentsOfOrientation(deviceState);
     }
 
     @Override
@@ -152,266 +117,47 @@
         launcher.onAssistantVisibilityChanged(visibility);
     }
 
-    @NonNull
     @Override
-    public HomeAnimationFactory prepareHomeUI() {
-        Launcher launcher = getCreatedActivity();
-        final DeviceProfile dp = launcher.getDeviceProfile();
-        final RecentsView recentsView = launcher.getOverviewPanel();
-        final TaskView runningTaskView = recentsView.getRunningTaskView();
-        final View workspaceView;
-        if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
-            workspaceView = launcher.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(launcher, workspaceView,
-                        true /* hideOriginal */, iconLocation, false /* isOpening */)
-                : null;
-        setLauncherHideBackArrow(true);
-        return new HomeAnimationFactory() {
-            @Nullable
-            @Override
-            public View getFloatingView() {
-                return floatingIconView;
-            }
-
-            @NonNull
-            @Override
-            public RectF getWindowTargetRect() {
-                if (canUseWorkspaceView) {
-                    return iconLocation;
-                } else {
-                    return HomeAnimationFactory
-                        .getDefaultWindowTargetRect(recentsView.getPagedOrientationHandler(), 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 launcher.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
-                        0 /* animComponents */);
-            }
-
-            @Override
-            public void playAtomicAnimation(float velocity) {
-                new StaggeredWorkspaceAnim(launcher, velocity, true /* animateOverviewScrim */)
-                        .start();
-            }
-        };
-    }
-
-    @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        BaseQuickstepLauncher launcher = getCreatedActivity();
-        final LauncherState startState = launcher.getStateManager().getState();
-
-        LauncherState resetState = startState;
-        if (startState.disableRestore) {
-            resetState = launcher.getStateManager().getRestState();
-        }
-        launcher.getStateManager().setRestState(resetState);
-
-        final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
-        launcher.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.
-        launcher.getAppsView().reset(false /* animate */);
-
-        return new AnimationFactory() {
-            private final ShelfPeekAnim mShelfAnim = launcher.getShelfPeekAnim();
-            private boolean mIsAttachedToWindow;
-
-            @Override
-            public void createActivityInterface(long transitionLength) {
-                createActivityInterfaceInternal(launcher, 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(launcher) == Mode.NO_BUTTON) {
-                    setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
-                }
-            }
-
-            @Override
-            public void adjustActivityControllerInterpolators() {
-                if (mAdjustInterpolatorsRunnable != null) {
-                    mAdjustInterpolatorsRunnable.run();
-                    mAdjustInterpolatorsRunnable = null;
-                }
-            }
-
-            @Override
-            public void onTransitionCancelled() {
-                launcher.getStateManager().goToState(startState, false /* animate */);
-            }
-
+    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) {
-                mShelfAnim.setShelfState(shelfState, interpolator, duration);
+                mActivity.getShelfPeekAnim().setShelfState(shelfState, interpolator, duration);
             }
 
             @Override
-            public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
-                if (mIsAttachedToWindow == attached && animate) {
-                    return;
+            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)));
                 }
-                mIsAttachedToWindow = attached;
-                LauncherRecentsView recentsView = launcher.getOverviewPanel();
-                Animator fadeAnim = launcher.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);
+                // 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);
 
-                    float fromTranslation = attached ? offscreenX - scrollOffsetX : 0;
-                    float toTranslation = attached ? 0 : offscreenX - scrollOffsetX;
-                    launcher.getStateManager()
-                            .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-
-                    PagedOrientationHandler pagedOrientationHandler =
-                        recentsView.getPagedViewOrientedState().getOrientationHandler();
-                    if (!recentsView.isShown() && animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, fromTranslation);
-                    } else {
-                        fromTranslation =
-                            pagedOrientationHandler.getPrimaryViewTranslate().get(recentsView);
-                    }
-
-                    if (!animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, toTranslation);
-                    } else {
-                        launcher.getStateManager().createStateElementAnimation(
-                                INDEX_RECENTS_TRANSLATE_X_ANIM,
-                                fromTranslation, toTranslation).start();
-                    }
-
-                    fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
-                } else {
-                    fadeAnim.setInterpolator(ACCEL_DEACCEL);
-                }
-                fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
             }
         };
-    }
 
-    private void createActivityInterfaceInternal(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)));
-        }
-
-        // Animate the blur and wallpaper zoom
-        DepthController depthController = getDepthController();
-        float fromDepthRatio = fromState.getDepth(activity);
-        float toDepthRatio = endState.getDepth(activity);
-        Animator depthAnimator = ObjectAnimator.ofFloat(depthController,
-                new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
-                fromDepthRatio, toDepthRatio);
-        anim.play(depthAnimator);
-
-        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);
-            });
-        };
-
-        // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
-        float pullbackStartProgress = (0.75f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
-        float pullbackMaxProgress = (0.5f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
-        mSwipeUpPullbackStartAndMaxProgress = new Pair<>(
-                pullbackStartProgress, pullbackMaxProgress);
+        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
@@ -420,6 +166,15 @@
                 onInitListener.test(alreadyOnHome));
     }
 
+    @Override
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        launcher.setOnDeferredActivityLaunchCallback(r);
+    }
+
     @Nullable
     @Override
     public BaseQuickstepLauncher getCreatedActivity() {
@@ -427,11 +182,13 @@
     }
 
     @Nullable
-    @UiThread
-    private Launcher getVisibleLauncher() {
-        Launcher launcher = getCreatedActivity();
-        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
-                launcher : null;
+    @Override
+    public DepthController getDepthController() {
+        BaseQuickstepLauncher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return null;
+        }
+        return launcher.getDepthController();
     }
 
     @Nullable
@@ -442,8 +199,19 @@
                 ? 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;
@@ -458,9 +226,29 @@
         return true;
     }
 
+
     @Override
-    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
-        return deviceState.isInDeferredGestureRegion(ev);
+    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
@@ -469,11 +257,21 @@
     }
 
     @Override
-    public boolean shouldMinimizeSplitScreen() {
+    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
@@ -497,15 +295,6 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.getStateManager().moveToRestState();
-    }
-
-    @Override
     public void closeOverlay() {
         Launcher launcher = getCreatedActivity();
         if (launcher == null) {
@@ -520,48 +309,37 @@
     }
 
     @Override
-    public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable onFinishRunnable) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        RecentsView recentsView = launcher.getOverviewPanel();
-        if (recentsView == null) {
-            if (onFinishRunnable != null) {
-                onFinishRunnable.run();
+    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);
             }
-            return;
         }
-        recentsView.switchToScreenshot(thumbnailData, onFinishRunnable);
     }
 
-    @Override
-    public void setOnDeferredActivityLaunchCallback(Runnable r) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.setOnDeferredActivityLaunchCallback(r);
-    }
-
-    @Override
-    public void updateOverviewPredictionState() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
-                PredictionUiStateManager.Client.OVERVIEW);
-    }
-
-    @Nullable
-    @Override
-    public DepthController getDepthController() {
-        BaseQuickstepLauncher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return null;
-        }
-        return launcher.getDepthController();
-    }
 }
\ 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/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index c6b719a..434a929 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -28,9 +28,10 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.BinderThread;
-import com.android.launcher3.BaseDraggingActivity;
+
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
@@ -111,7 +112,7 @@
         protected void onTransitionComplete() {
             // TODO(b/138729100) This doesn't execute first time launcher is run
             if (mTriggeredFromAltTab) {
-                RecentsView rv = (RecentsView) mActivityInterface.getVisibleRecentsView();
+                RecentsView rv =  mActivityInterface.getVisibleRecentsView();
                 if (rv == null) {
                     return;
                 }
@@ -136,7 +137,7 @@
 
         @Override
         protected boolean handleCommand(long elapsedTime) {
-            RecentsView recents = (RecentsView) mActivityInterface.getVisibleRecentsView();
+            RecentsView recents = mActivityInterface.getVisibleRecentsView();
             if (recents == null) {
                 return false;
             }
@@ -150,9 +151,9 @@
         }
     }
 
-    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
+    private class RecentsActivityCommand<T extends StatefulActivity<?>> implements Runnable {
 
-        protected final BaseActivityInterface<T> mActivityInterface;
+        protected final BaseActivityInterface<?, T> mActivityInterface;
         private final long mCreateTime;
         private final AppToOverviewAnimationProvider<T> mAnimationProvider;
 
@@ -164,7 +165,7 @@
             mActivityInterface = mOverviewComponentObserver.getActivityInterface();
             mCreateTime = SystemClock.elapsedRealtime();
             mAnimationProvider = new AppToOverviewAnimationProvider<>(mActivityInterface,
-                    RecentsModel.getRunningTaskId());
+                    RecentsModel.getRunningTaskId(), mDeviceState);
 
             // Preload the plan
             mRecentsModel.getTasks(null);
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 d4c746f..6f4d34c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -4,20 +4,18 @@
 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.systemui.shared.recents.model.Task;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 public class QuickstepTestInformationHandler extends TestInformationHandler {
 
-    private final Context mContext;
+    protected final Context mContext;
+
     public QuickstepTestInformationHandler(Context context) {
         mContext = context;
     }
@@ -26,6 +24,15 @@
     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);
@@ -35,7 +42,8 @@
 
             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;
             }
@@ -45,26 +53,6 @@
                         Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
             }
 
-            case TestProtocol.REQUEST_RECENT_TASKS_LIST: {
-                ArrayList<String> taskBaseIntentComponents = new ArrayList<>();
-                CountDownLatch latch = new CountDownLatch(1);
-                RecentsModel.INSTANCE.get(mContext).getTasks((tasks) -> {
-                    for (Task t : tasks) {
-                        taskBaseIntentComponents.add(
-                                t.key.baseIntent.getComponent().flattenToString());
-                    }
-                    latch.countDown();
-                });
-                try {
-                    latch.await(2, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-                response.putStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                        taskBaseIntentComponents);
-                return response;
-            }
-
             case TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED: {
                 response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
@@ -77,12 +65,13 @@
 
     @Override
     protected Activity getCurrentActivity() {
-        OverviewComponentObserver observer = new OverviewComponentObserver(mContext,
-                new RecentsAnimationDeviceState(mContext));
+        RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
+        OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
         try {
             return observer.getActivityInterface().getCreatedActivity();
         } finally {
             observer.onDestroy();
+            rads.destroy();
         }
     }
 
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 42d944f..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,12 +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.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
-import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
+import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.Animator;
@@ -29,48 +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.util.ObjectWrapper;
+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.AppWindowAnimationHelper;
+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.recreateControllers();
+        mActionsView = findViewById(R.id.overview_actions_view);
+
+        mDragLayer.recreateControllers();
+        mFallbackRecentsView.init(mActionsView);
     }
 
     @Override
@@ -79,59 +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) {
-                ObjectWrapper<ThumbnailData> obj = (ObjectWrapper<ThumbnailData>) thumbnail;
-                ThumbnailData thumbnailData = obj.get();
-                mFallbackRecentsView.showCurrentTask(taskID);
-                mFallbackRecentsView.updateThumbnail(taskID, thumbnailData);
-                // Clear the ref since any reference to the extras on the system side will still
-                // hold a reference to the wrapper
-                obj.clear();
-            }
-        }
-        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.recreateControllers();
+        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
@@ -139,6 +153,10 @@
         return (T) mFallbackRecentsView;
     }
 
+    public OverviewActionsView getActionsView() {
+        return mActionsView;
+    }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
@@ -160,12 +178,7 @@
                     RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
                 AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
                         wallpaperTargets);
-                anim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mFallbackRecentsView.resetViewUI();
-                    }
-                });
+                anim.addListener(resetStateListener());
                 result.setAnimation(anim, RecentsActivity.this);
             }
         };
@@ -183,25 +196,18 @@
             RemoteAnimationTargetCompat[] wallpaperTargets) {
         AnimatorSet target = new AnimatorSet();
         boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
-        AppWindowAnimationHelper helper = new AppWindowAnimationHelper(
-            mFallbackRecentsView.getPagedViewOrientedState(), this);
-        Animator recentsAnimator = getRecentsWindowAnimator(taskView, !activityClosing, appTargets,
-                wallpaperTargets, null /* depthController */,
-                helper);
-        target.play(recentsAnimator.setDuration(RECENTS_LAUNCH_DURATION));
+        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;
@@ -213,13 +219,14 @@
         // 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);
     }
 
     @Override
@@ -228,7 +235,102 @@
         AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
     }
 
-    public void onTaskLaunched() {
-        mFallbackRecentsView.resetTaskVisuals();
+    @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/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 fbf29af..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,20 +16,31 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-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;
@@ -54,6 +65,28 @@
                 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;
     }
 
@@ -84,45 +117,68 @@
     /**
      * Overlay on each task handling Overview Action Buttons.
      */
-    public static class TaskOverlay {
+    public static class TaskOverlay<T extends OverviewActionsView> {
 
         private final Context mApplicationContext;
-        private OverviewActionsView mActionsView;
-        private final TaskThumbnailView mThumbnailView;
+        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) {
-            ImageActionsApi imageApi = new ImageActionsApi(
-                    mApplicationContext, mThumbnailView::getThumbnail);
+        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+                boolean rotated) {
+            final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
 
-            if (mActionsView == null && ENABLE_OVERVIEW_ACTIONS.get()
-                    && SysUINavigationMode.removeShelfFromOverview(mApplicationContext)) {
-                mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
-                        R.id.overview_actions_view);
-            }
-            if (mActionsView != null) {
-                mActionsView.setListener(new OverviewActionsView.Listener() {
-                    @Override
-                    public void onShare() {
-                        imageApi.startShareActivity();
+            getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+
+            getActionsView().setCallbacks(new OverlayUICallbacks() {
+                @Override
+                public void onShare() {
+                    if (isAllowedByPolicy) {
+                        mImageApi.startShareActivity();
+                    } else {
+                        showBlockedByPolicyMessage();
                     }
+                }
 
-                    @Override
-                    public void onScreenshot() {
-                        imageApi.saveScreenshot(mThumbnailView.getThumbnail(),
-                                getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key.id);
-                    }
-                });
+                @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();
             }
-
         }
 
         /**
@@ -132,13 +188,26 @@
         }
 
         /**
-         * Whether the overlay is modal, which means only tapping is enabled, but no swiping.
+         * Called when the system wants to reset the modal visuals.
          */
-        public boolean isOverlayModal() {
-            return false;
+        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.
@@ -156,9 +225,44 @@
          *
          * @return the insets in screen coordinates.
          */
+        @RequiresApi(api = Build.VERSION_CODES.Q)
         public Insets getTaskSnapshotInsets() {
-            // TODO: return the real insets
-            return Insets.of(0, 0, 0, 0);
+            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
index 9ba2e5a..ff051b6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
@@ -18,24 +18,25 @@
 
 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.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.UserHandle;
 import android.view.View;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
+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;
@@ -65,28 +66,18 @@
 
     SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
 
-    static WorkspaceItemInfo dummyInfo(TaskView view) {
-        Task task = view.getTask();
-
-        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
-        dummyInfo.intent = new Intent();
-        ComponentName component = task.getTopComponent();
-        dummyInfo.intent.setComponent(component);
-        dummyInfo.user = UserHandle.of(task.key.userId);
-        dummyInfo.title = TaskUtils.getTitle(view.getContext(), task);
-        return dummyInfo;
-    }
-
-    TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, dummyInfo(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) {
+        MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) {
             mIconRes = iconRes;
             mTextRes = textRes;
+            mLauncherEvent = launcherEvent;
         }
 
         protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
@@ -102,7 +93,8 @@
             if (!isAvailable(activity, task.key.displayId)) {
                 return null;
             }
-            return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this);
+            return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this,
+                    mLauncherEvent);
         }
     }
 
@@ -114,11 +106,12 @@
         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) {
-            super(iconRes, textRes, activity, dummyInfo(taskView));
-
+        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();
@@ -203,12 +196,14 @@
                 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) {
+    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) {
@@ -241,8 +236,8 @@
         }
     };
 
-    TaskShortcutFactory FREE_FORM = new MultiWindowFactory(
-            R.drawable.ic_split_screen, R.string.recent_task_option_freeform) {
+    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) {
@@ -286,7 +281,7 @@
         private final TaskView mTaskView;
 
         public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
-            super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, dummyInfo(tv));
+            super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo());
             mTaskView = tv;
         }
 
@@ -302,14 +297,31 @@
             };
             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, dummyInfo(view)) : null;
+                    ? new SystemShortcut.Install(activity, view.getItemInfo()) : null;
 
     TaskShortcutFactory WELLBEING = (activity, view) ->
-            WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, dummyInfo(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/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
index 6a3e1fe..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,43 +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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+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.AnimatorSet;
-import android.animation.ObjectAnimator;
-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.launcher3.DeviceProfile;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.quickstep.util.AppWindowAnimationHelper;
-import com.android.quickstep.util.MultiValueUpdateListener;
+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;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 
 /**
  * Utility class for helpful methods related to {@link TaskView} objects and their tasks.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public final class TaskViewUtils {
 
     private TaskViewUtils() {}
@@ -117,98 +120,110 @@
     }
 
     /**
-     * @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 Animator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+    public static void createRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
             RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets,
-            DepthController depthController,
-            final AppWindowAnimationHelper inOutHelper) {
-        SyncRtSurfaceTransactionApplierCompat applier =
-                new SyncRtSurfaceTransactionApplierCompat(v);
+            RemoteAnimationTargetCompat[] wallpaperTargets, DepthController depthController,
+            PendingAnimation out) {
+
+        SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
         final RemoteAnimationTargets targets =
                 new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_OPENING);
-        targets.addDependentTransactionApplier(applier);
-        AppWindowAnimationHelper.TransformParams params =
-                new AppWindowAnimationHelper.TransformParams()
+        targets.addReleaseCheck(applier);
+
+        TransformParams params = new TransformParams()
                     .setSyncTransactionApplier(applier)
-                    .setTargetSet(targets)
-                    .setLauncherOnTop(true);
+                    .setTargetSet(targets);
 
-        AnimatorSet animatorSet = new AnimatorSet();
         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);
-            final RectF mThumbnailRect;
+        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;
 
-            {
-                inOutHelper.setTaskAlphaCallback((t, alpha) -> mTaskAlpha.value);
+        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);
 
-                inOutHelper.prepareAnimation(
-                        BaseActivity.fromContext(v.getContext()).getDeviceProfile(),
-                        true /* isOpening */);
-                inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(),
-                        targets.apps.length == 0 ? null : targets.apps[0]);
+            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);
 
-                mThumbnailRect = new RectF(inOutHelper.getTargetRect());
-                mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY());
-                Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX());
-            }
+            out.addOnFrameCallback(() -> tsv.apply(params));
+            topMostSimulator = tsv;
+        }
 
-            @Override
-            public void onUpdate(float percent) {
-                // TODO: Take into account the current fullscreen progress for animating the insets
-                params.setProgress(1 - percent);
-                RectF taskBounds;
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                    List<SurfaceParams> surfaceParamsList = new ArrayList<>();
-                    // Append the surface transform params for the app that's being opened.
-                    Collections.addAll(surfaceParamsList, inOutHelper.computeSurfaceParams(params));
+        // Fade in the task during the initial 20% of the animation
+        out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1, clampToProgress(LINEAR, 0, 0.2f));
 
-                    AppWindowAnimationHelper liveTileAnimationHelper =
-                            v.getRecentsView().getClipAnimationHelper();
-                    if (liveTileAnimationHelper != null) {
-                        // Append the surface transform params for the live tile app.
-                        AppWindowAnimationHelper.TransformParams liveTileParams =
-                                v.getRecentsView().getLiveTileParams(true /* mightNeedToRefill */);
-                        if (liveTileParams != null) {
-                            SurfaceParams[] liveTileSurfaceParams =
-                                    liveTileAnimationHelper.computeSurfaceParams(liveTileParams);
-                            if (liveTileSurfaceParams != null) {
-                                Collections.addAll(surfaceParamsList, liveTileSurfaceParams);
-                            }
-                        }
-                    }
-                    // Apply surface transform using the surface params list.
-                    AppWindowAnimationHelper.applySurfaceParams(params.getSyncTransactionApplier(),
-                            surfaceParamsList.toArray(new SurfaceParams[surfaceParamsList.size()]));
-                    // Get the task bounds for the app that's being opened after surface transform
-                    // update.
-                    taskBounds = inOutHelper.updateCurrentRect(params);
-                } else {
-                    taskBounds = inOutHelper.applyTransform(params);
+        if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulator != null) {
+            out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
+
+            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);
                 }
+            });
+        }
 
-                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);
-                }
-            }
-        });
-        appAnimator.addListener(new AnimatorListenerAdapter() {
+        out.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 targets.release();
@@ -216,12 +231,8 @@
         });
 
         if (depthController != null) {
-            ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController,
-                    DEPTH, BACKGROUND_APP.getDepth(v.getContext()));
-            animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
-        } else {
-            animatorSet.play(appAnimator);
+            out.setFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(context),
+                    TOUCH_RESPONSE_INTERPOLATOR);
         }
-        return animatorSet;
     }
 }
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 c889632..a3705fd 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -15,6 +15,7 @@
  */
 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;
@@ -26,18 +27,20 @@
 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_TRACING_ENABLED;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
 
 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.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
+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;
@@ -46,6 +49,7 @@
 import android.view.Choreographer;
 import android.view.InputEvent;
 import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.Nullable;
@@ -53,31 +57,37 @@
 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.allapps.DiscoveryBounce;
 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.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.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
 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.views.RecentsView;
+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;
@@ -86,7 +96,6 @@
 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.RecentsAnimationListener;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 
 import java.io.FileDescriptor;
@@ -115,7 +124,7 @@
 /**
  * Service connected by system-UI for handling touch interaction.
  */
-@TargetApi(Build.VERSION_CODES.Q)
+@TargetApi(Build.VERSION_CODES.R)
 public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin>,
         ProtoTraceable<LauncherTraceProto> {
 
@@ -126,6 +135,12 @@
     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;
@@ -220,6 +235,11 @@
             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 **/
         public void onQuickStep(MotionEvent motionEvent) { }
 
@@ -279,6 +299,7 @@
         mAM = ActivityManagerWrapper.getInstance();
         mDeviceState = new RecentsAnimationDeviceState(this);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+        mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
         ProtoTracer.INSTANCE.get(this).add(this);
 
@@ -289,9 +310,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();
@@ -300,25 +318,16 @@
     }
 
     private void initInputMonitor() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
-        }
         disposeEventHandlers();
         if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
             return;
         }
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 2");
-        }
 
         Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
                 mDeviceState.getDisplayId());
         mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 3");
-        }
 
         mDeviceState.updateGestureTouchRegions();
     }
@@ -331,6 +340,13 @@
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
     }
 
+    /**
+     * Called when the one handed mode overlay package changes, to recreate touch region.
+     */
+    private void onOneHandedModeOverlayChanged(int newGesturalHeight) {
+        initInputMonitor();
+    }
+
     @UiThread
     public void onUserUnlocked() {
         mTaskAnimationManager = new TaskAnimationManager();
@@ -351,6 +367,9 @@
 
         PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
                 OverscrollPlugin.class, false /* allowMultiple */);
+
+        mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
+        onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
     }
 
     private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
@@ -365,11 +384,29 @@
         if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) {
             sharedPrefs.edit()
                     .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
-                    .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false)
+                    .putBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN, false)
                     .apply();
         }
     }
 
+    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 (mDeviceState.isUserUnlocked()) {
@@ -408,6 +445,9 @@
         ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
         ProtoTracer.INSTANCE.get(this).remove(this);
 
+        getSystemService(AccessibilityManager.class)
+                .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+
         sConnected = false;
         super.onDestroy();
     }
@@ -419,9 +459,6 @@
     }
 
     private void onInputEvent(InputEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onInputEvent " + ev);
-        }
         if (!(ev instanceof MotionEvent)) {
             Log.e(TAG, "Unknown event " + ev);
             return;
@@ -440,38 +477,49 @@
 
         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);
-            GestureState newGestureState;
 
             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);
-                newGestureState = createGestureState();
+                GestureState newGestureState = createGestureState(mGestureState);
                 mConsumer.onConsumerAboutToBeSwitched();
-                mConsumer = newConsumer(prevGestureState, newGestureState, event);
+                mGestureState = newGestureState;
+                mConsumer = newConsumer(prevGestureState, mGestureState, event);
 
-                ActiveGestureLog.INSTANCE.addLog("setInputConsumer", mConsumer.getType());
+                ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName());
                 mUncheckedConsumer = mConsumer;
-            } else if (mDeviceState.isUserUnlocked()
-                    && mDeviceState.isFullyGesturalNavMode()
-                    && mDeviceState.canTriggerAssistantAction(event)) {
-                newGestureState = createGestureState();
-                // 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,
-                    newGestureState,
-                    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 if (mDeviceState.canTriggerOneHandedAction(event)
+                    && !mDeviceState.isOneHandedModeActive()) {
+                // Consume gesture event for triggering one handed feature.
+                mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+                        InputConsumer.NO_OP, mInputMonitorCompat);
             } else {
-                newGestureState = DEFAULT_STATE;
                 mUncheckedConsumer = InputConsumer.NO_OP;
             }
-
-            // Save the current gesture state
-            mGestureState = newGestureState;
         } else {
             // Other events
             if (mUncheckedConsumer != InputConsumer.NO_OP) {
@@ -480,28 +528,51 @@
             }
         }
 
-        ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
-        mUncheckedConsumer.onMotionEvent(event);
-
-        if (action == ACTION_UP || action == ACTION_CANCEL) {
-            if (mConsumer != null && !mConsumer.isConsumerDetachedFromGesture()) {
-                onConsumerInactive(mConsumer);
+        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 GestureState createGestureState() {
+    private GestureState createGestureState(GestureState previousGestureState) {
         GestureState gestureState = new GestureState(mOverviewComponentObserver,
                 ActiveGestureLog.INSTANCE.generateAndSetLogId());
-        gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
-                () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
+        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 InputConsumer newConsumer(GestureState previousGestureState,
             GestureState newGestureState, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer");
+        }
         boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
 
         if (!mDeviceState.isUserUnlocked()) {
@@ -513,6 +584,9 @@
                 return mResetGestureInputConsumer;
             }
         }
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer:user is unlocked");
+        }
 
         // 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.
@@ -520,12 +594,11 @@
                 || previousGestureState.isRecentsAnimationRunning()
                         ? newBaseConsumer(previousGestureState, newGestureState, event)
                         : mResetGestureInputConsumer;
-        // TODO(b/149880412): 2 button landscape mode is wrecked. Fixit!
         if (mDeviceState.isGesturalNavMode()) {
             handleOrientationSetup(base);
         }
         if (mDeviceState.isFullyGesturalNavMode()) {
-            if (mDeviceState.canTriggerAssistantAction(event)) {
+            if (mDeviceState.canTriggerAssistantAction(event, newGestureState.getRunningTask())) {
                 base = new AssistantInputConsumer(
                     this,
                     newGestureState,
@@ -555,12 +628,24 @@
                 }
             }
 
+            // 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, newGestureState);
             }
 
+            if (mDeviceState.canTriggerOneHandedAction(event)) {
+                base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+                        mInputMonitorCompat);
+            }
+
             if (mDeviceState.isAccessibilityMenuAvailable()) {
                 base = new AccessibilityInputConsumer(this, mDeviceState, base,
                         mInputMonitorCompat);
@@ -569,24 +654,21 @@
             if (mDeviceState.isScreenPinningActive()) {
                 base = mResetGestureInputConsumer;
             }
+
+            if (mDeviceState.canTriggerOneHandedAction(event)) {
+                base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+                        mInputMonitorCompat);
+            }
         }
         return base;
     }
 
     private void handleOrientationSetup(InputConsumer baseInputConsumer) {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
-            return;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "handleOrientationSetup.1");
         }
-        mDeviceState.enableMultipleRegions(baseInputConsumer instanceof OtherActivityInputConsumer);
-        BaseDraggingActivity activity =
-                mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
-        if (activity == null || !(activity.getOverviewPanel() instanceof RecentsView)) {
-            return;
-        }
-        ((RecentsView) activity.getOverviewPanel())
-            .setLayoutRotation(mDeviceState.getCurrentActiveRotation(),
-                mDeviceState.getDisplayRotation());
-        activity.getDragLayer().recreateControllers();
+
+        baseInputConsumer.notifyOrientationSetup();
     }
 
     private InputConsumer newBaseConsumer(GestureState previousGestureState,
@@ -596,19 +678,23 @@
             return createDeviceLockedInputConsumer(gestureState);
         }
 
-        RunningTaskInfo runningTask = gestureState.getRunningTask();
-        ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
-        boolean forceOverviewInputConsumer = runningTask != null
-                && runningTask.baseIntent.getComponent().equals(homeComponent);
+        // 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
+            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
+                    () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
+            ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
+            ComponentName runningComponent =
+                    gestureState.getRunningTask().baseIntent.getComponent();
+            forceOverviewInputConsumer =
+                    runningComponent != null && runningComponent.equals(homeComponent);
+        }
 
-        if (previousGestureState.getFinishingRecentsAnimationTaskId() > 0) {
-            // 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 = previousGestureState.getFinishingRecentsAnimationTaskId();
-            gestureState.updateRunningTask(info);
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
-        } else if (gestureState.getRunningTask() == null) {
+        if (gestureState.getRunningTask() == null) {
             return mResetGestureInputConsumer;
         } else if (previousGestureState.isRunningAnimationToLauncher()
                 || gestureState.getActivityInterface().isResumed()
@@ -622,25 +708,22 @@
         } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
             return mResetGestureInputConsumer;
         } else {
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
+            return createOtherActivityInputConsumer(gestureState, event);
         }
     }
 
-    private InputConsumer createOtherActivityInputConsumer(GestureState previousGestureState,
-            GestureState gestureState, MotionEvent event) {
+    private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
+            MotionEvent event) {
 
-        final boolean shouldDefer;
         final BaseSwipeUpHandler.Factory factory;
-
         if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            shouldDefer = previousGestureState.getFinishingRecentsAnimationTaskId() < 0;
             factory = mFallbackSwipeHandlerFactory;
         } else {
-            shouldDefer = gestureState.getActivityInterface().deferStartingActivity(mDeviceState,
-                    event);
             factory = mLauncherSwipeHandlerFactory;
         }
 
+        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,
@@ -659,7 +742,7 @@
     public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
             GestureState gestureState, MotionEvent event,
             boolean forceOverviewInputConsumer) {
-        BaseDraggingActivity activity = gestureState.getActivityInterface().getCreatedActivity();
+        StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity();
         if (activity == null) {
             return mResetGestureInputConsumer;
         }
@@ -678,38 +761,47 @@
     }
 
     /**
-     * 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 != null && mConsumer.isInConsumerHierarchy(caller)) {
-            mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
-            mGestureState = new GestureState();
+        if (mConsumer != null && mConsumer.getActiveConsumerInHierarchy() == caller) {
+            reset();
         }
     }
 
+    private void reset() {
+        mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
+        mGestureState = DEFAULT_STATE;
+    }
+
     private void preloadOverview(boolean fromInit) {
         if (!mDeviceState.isUserUnlocked()) {
             return;
         }
+
         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 BaseActivityInterface<BaseDraggingActivity> activityInterface =
+        final BaseActivityInterface activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
+        final Intent overviewIntent = new Intent(
+                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
         if (activityInterface.getCreatedActivity() == null) {
             // Make sure that UI states will be initialized.
             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
@@ -717,8 +809,7 @@
             return;
         }
 
-        mTaskAnimationManager.preloadRecentsAnimation(
-                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
+        mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
     }
 
     @Override
@@ -735,6 +826,13 @@
         }
         if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
                 activity.getResources().getConfiguration().diff(newConfig))) {
+            // Since navBar gestural height are different between portrait and landscape,
+            // can handle orientation changes and refresh navigation gestural region through
+            // onOneHandedModeChanged()
+            int newGesturalHeight = ResourceUtils.getNavbarSize(
+                    ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
+                    getApplicationContext().getResources());
+            mDeviceState.onOneHandedModeChanged(newGesturalHeight);
             return;
         }
 
@@ -767,6 +865,7 @@
             if (mGestureState != null) {
                 mGestureState.dump(pw);
             }
+            SysUINavigationMode.INSTANCE.get(this).dump(pw);
             pw.println("TouchState:");
             BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
                     : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
@@ -795,16 +894,16 @@
         }
     }
 
-    private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return  new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+    private BaseSwipeUpHandler createLauncherSwipeHandler(
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+        return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new FallbackSwipeHandler(this, mDeviceState, gestureState,
-                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() {
@@ -823,11 +922,6 @@
         }
     }
 
-    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;
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
index 6f919c1..be3fdde 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.fallback;
 
+import android.graphics.PointF;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -30,7 +31,8 @@
 /**
  * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
  */
-public class FallbackNavBarTouchController implements TouchController {
+public class FallbackNavBarTouchController implements TouchController,
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
 
     private final RecentsActivity mActivity;
     @Nullable
@@ -44,7 +46,7 @@
                     DefaultDisplay.INSTANCE.get(mActivity).getInfo());
             mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
                     true /* disableHorizontalSwipe */, navBarPosition,
-                    null /* onInterceptTouch */, this::onSwipeUp);
+                    null /* onInterceptTouch */, this);
         } else {
             mTriggerSwipeUpTracker = null;
         }
@@ -72,7 +74,11 @@
         return false;
     }
 
-    private void onSwipeUp(boolean wasFling) {
+    @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 dc0c194..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 = getTempAppWindowAnimationHelper()
-                    .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,7 +134,7 @@
             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;
             }
         }
@@ -197,7 +142,44 @@
     }
 
     @Override
-    protected boolean supportsVerticalLandscape() {
-        return false;
+    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 7f5ec9b..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ /dev/null
@@ -1,93 +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.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 Point mLastKnownSize = new Point(MIN_SIZE, MIN_SIZE);
-
-    public RecentsRootView(Context context, AttributeSet attrs) {
-        super(context, attrs, 1 /* alphaChannelCount */);
-        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | SYSTEM_UI_FLAG_LAYOUT_STABLE);
-    }
-
-    public Point getLastKnownSize() {
-        return mLastKnownSize;
-    }
-
-    @Override
-    public void recreateControllers() {
-        mControllers = new TouchController[] {
-                new RecentsTaskController(mActivity),
-                new FallbackNavBarTouchController(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/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index bcc9707..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
@@ -25,13 +25,11 @@
     }
 
     @Override
-    public boolean isConsumerDetachedFromGesture() {
-        return mDelegate.isConsumerDetachedFromGesture();
-    }
-
-    @Override
-    public boolean isInConsumerHierarchy(InputConsumer candidate) {
-        return this == candidate || mDelegate.isInConsumerHierarchy(candidate);
+    public InputConsumer getActiveConsumerInHierarchy() {
+        if (mState == STATE_ACTIVE) {
+            return this;
+        }
+        return mDelegate.getActiveConsumerInHierarchy();
     }
 
     @Override
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 7b8d40c..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,46 +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.LauncherSwipeHandler.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.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.AnimatedFloat;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.LockScreenRecentsActivity;
 import com.android.quickstep.MultiStateCallback;
+import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.AppWindowAnimationHelper;
+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,
-        RecentsAnimationCallbacks.RecentsAnimationListener {
-
-    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) {
@@ -83,18 +86,20 @@
     private final InputMonitorCompat mInputMonitorCompat;
 
     private final PointF mTouchDown = new PointF();
-    private final AppWindowAnimationHelper mAppWindowAnimationHelper;
-    private final AppWindowAnimationHelper.TransformParams mTransformParams;
-    private final Point mDisplaySize;
+    private final TransformParams mTransformParams;
     private final MultiStateCallback mStateCallback;
 
+    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 RecentsAnimationController mRecentsAnimationController;
-    private RecentsAnimationTargets mRecentsAnimationTargets;
 
     public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
@@ -104,9 +109,10 @@
         mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
         mTouchSlopSquared = squaredTouchSlop(context);
-        mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
-        mTransformParams = new AppWindowAnimationHelper.TransformParams();
+        mTransformParams = new TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
+        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;
@@ -157,9 +163,7 @@
                     }
                 } else {
                     float dy = Math.max(mTouchDown.y - y, 0);
-                    mProgress = dy / mDisplaySize.y;
-                    mTransformParams.setProgress(mProgress);
-                    mAppWindowAnimationHelper.applyTransform(mTransformParams);
+                    mProgress.updateValue(dy / mDisplaySize.y);
                 }
                 break;
             }
@@ -175,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());
@@ -189,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;
@@ -204,13 +222,11 @@
 
     private void startRecentsTransition() {
         mThresholdCrossed = true;
+        mHomeLaunched = false;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitorCompat.pilferPointers();
 
-        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)
+        Intent intent = mGestureState.getHomeIntent()
                 .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
         mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
     }
@@ -219,38 +235,40 @@
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
         mRecentsAnimationController = controller;
-        mRecentsAnimationTargets = targets;
-
-        Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
-        RemoteAnimationTargetCompat targetCompat = targets.findTask(
-                mGestureState.getRunningTaskId());
-        if (targetCompat != null) {
-            mAppWindowAnimationHelper.updateSource(displaySize, targetCompat);
-        }
-
-        Utilities.scaleRectAboutCenter(displaySize, SCALE_DOWN);
-        displaySize.offsetTo(displaySize.left, 0);
-        mTransformParams.setTargetSet(mRecentsAnimationTargets)
-                .setLauncherOnTop(true);
-        mAppWindowAnimationHelper.updateTargetRect(displaySize);
-        mAppWindowAnimationHelper.applyTransform(mTransformParams);
-
+        mTransformParams.setTargetSet(targets);
+        applyTransform();
         mStateCallback.setState(STATE_TARGET_RECEIVED);
     }
 
     @Override
     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
         mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
+        mTransformParams.setTargetSet(null);
     }
 
     private void endRemoteAnimation() {
-        if (mRecentsAnimationController != null) {
+        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/OneHandedModeInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
new file mode 100644
index 0000000..0bb8fff
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.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.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 android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Touch consumer for handling gesture event to launch one handed
+ * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+ */
+public class OneHandedModeInputConsumer extends DelegateInputConsumer {
+
+    private static final String TAG = "OneHandedModeInputConsumer";
+    private static final int ANGLE_MAX = 150;
+    private static final int ANGLE_MIN = 30;
+
+    private final Context mContext;
+    private final RecentsAnimationDeviceState mDeviceState;
+
+    private final float mDragDistThreshold;
+    private final float mSquaredSlop;
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private final PointF mStartDragPos = new PointF();
+
+    private boolean mPassedSlop;
+
+    public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor) {
+        super(delegate, inputMonitor);
+        mContext = context;
+        mDeviceState = deviceState;
+        mDragDistThreshold = context.getResources().getDimensionPixelSize(
+                R.dimen.gestures_onehanded_drag_threshold);
+        mSquaredSlop = Utilities.squaredTouchSlop(context);
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_ONE_HANDED | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
+            case ACTION_DOWN: {
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                break;
+            }
+            case ACTION_MOVE: {
+                if (mState == STATE_DELEGATE_ACTIVE) {
+                    break;
+                }
+                if (!mDelegate.allowInterceptByParent()) {
+                    mState = STATE_DELEGATE_ACTIVE;
+                    break;
+                }
+
+                mLastPos.set(ev.getX(), ev.getY());
+                if (!mPassedSlop) {
+                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+                            > mSquaredSlop) {
+                        mStartDragPos.set(mLastPos.x, mLastPos.y);
+                        if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
+                                || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) {
+                            mPassedSlop = true;
+                            setActive(ev);
+                        } else {
+                            mState = STATE_DELEGATE_ACTIVE;
+                        }
+                    }
+                } else {
+                    float distance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
+                            mLastPos.y - mStartDragPos.y);
+                    if (distance > mDragDistThreshold && mPassedSlop) {
+                        onStopGestureDetected();
+                    }
+                }
+                break;
+            }
+            case ACTION_UP:
+            case ACTION_CANCEL: {
+                if (mLastPos.y >= mStartDragPos.y && mPassedSlop) {
+                    onStartGestureDetected();
+                }
+
+                mPassedSlop = false;
+                mState = STATE_INACTIVE;
+                break;
+            }
+        }
+
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    private void onStartGestureDetected() {
+        if (mDeviceState.isOneHandedModeEnabled()) {
+            if (!mDeviceState.isOneHandedModeActive()) {
+                SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode();
+            }
+        } else if (mDeviceState.isSwipeToNotificationEnabled()) {
+            SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel();
+        }
+    }
+
+    private void onStopGestureDetected() {
+        if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) {
+            return;
+        }
+
+        SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode();
+    }
+
+    private boolean isValidStartAngle(float deltaX, float deltaY) {
+        final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+        return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
+    }
+
+    private boolean isValidExitAngle(float deltaX, float deltaY) {
+        final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+        return angle > ANGLE_MIN && angle < ANGLE_MAX;
+    }
+}
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 fe9ef2b..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
@@ -23,9 +23,12 @@
 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.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;
 
@@ -37,6 +40,7 @@
 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;
@@ -75,7 +79,8 @@
     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;
@@ -111,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;
@@ -146,10 +154,12 @@
 
         boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
-        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
 
-        float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
-        mSquaredTouchSlop = slop * slop;
+        float slopMultiplier = mDeviceState.isFullyGesturalNavMode()
+                ? QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL
+                : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
+        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+        mSquaredTouchSlop = slopMultiplier * mTouchSlop * mTouchSlop;
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
@@ -183,6 +193,10 @@
                 && !mRecentsViewDispatcher.hasConsumer()) {
             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);
@@ -206,7 +220,7 @@
                 // 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());
                 }
 
                 TraceHelper.INSTANCE.endSection(traceToken);
@@ -243,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) {
@@ -257,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
@@ -274,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);
                     }
                 }
 
@@ -303,13 +327,20 @@
             }
             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() {
+    private void notifyGestureStarted(boolean isLikelyToStartNewTask) {
         ActiveGestureLog.INSTANCE.addLog("startQuickstep");
         if (mInteractionHandler == null) {
             return;
@@ -322,26 +353,25 @@
                 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) {
+    private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
 
         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
-                mTaskAnimationManager.isRecentsAnimationRunning(), isLikelyToStartNewTask);
+                mTaskAnimationManager.isRecentsAnimationRunning());
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
-        mInteractionHandler.initWhenReady();
+        Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+        mInteractionHandler.initWhenReady(intent);
 
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
             mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
             mActiveCallbacks.addListener(mInteractionHandler);
             mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
-            notifyGestureStarted();
+            notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
-            Intent intent = mInteractionHandler.getLaunchIntent();
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
                     mInteractionHandler);
@@ -393,6 +423,11 @@
     }
 
     @Override
+    public void notifyOrientationSetup() {
+        mDeviceState.onStartGesture();
+    }
+
+    @Override
     public void onConsumerAboutToBeSwitched() {
         Preconditions.assertUIThread();
         mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
@@ -430,6 +465,6 @@
 
     @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
index 0a21413..fb420a2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,49 +24,61 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 
+import static java.lang.Math.abs;
+
 import android.content.Context;
 import android.graphics.PointF;
-import android.view.GestureDetector;
+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.LauncherRecentsView;
 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}.
- *
- * @param <T> Draggable activity subclass used by RecentsView
  */
-public class OverscrollInputConsumer<T extends BaseDraggingActivity> extends DelegateInputConsumer {
-
+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 float mFlingThresholdPx;
+    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 Context mContext;
     private final GestureState mGestureState;
     @Nullable
     private final OverscrollPlugin mPlugin;
-    private final GestureDetector mGestureDetector;
 
+    @Nullable
     private RecentsView mRecentsView;
 
     public OverscrollInputConsumer(Context context, GestureState gestureState,
@@ -75,19 +87,24 @@
 
         mAngleThreshold = context.getResources()
                 .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
-        mFlingThresholdPx = context.getResources()
-            .getDimension(R.dimen.gestures_overscroll_fling_threshold);
-        mContext = context;
+        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;
-        mGestureDetector = new GestureDetector(context, new FlingGestureListener());
 
-        gestureState.getActivityInterface().createActivityInitListener(this::onActivityInit)
-                .register();
+
+        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
@@ -95,20 +112,28 @@
         return TYPE_OVERSCROLL | mDelegate.getType();
     }
 
-    private boolean onActivityInit(Boolean alreadyOnHome) {
-        mRecentsView = mGestureState.getActivityInterface().getCreatedActivity().getOverviewPanel();
-
-        return true;
-    }
-
     @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: {
@@ -134,79 +159,95 @@
                 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));
 
-                if (!mPassedSlop) {
-                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
-                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
-                            > mSquaredSlop) {
-
-                        mPassedSlop = true;
-                        mStartDragPos.set(mLastPos.x, mLastPos.y);
-                        if (isOverscrolled()) {
-                            setActive(ev);
-
-                            if (mPlugin != null) {
-                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
-                            }
-                        } else {
-                            mState = STATE_DELEGATE_ACTIVE;
-                        }
-                    }
+                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);
                 }
 
-                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
-                        && mPlugin != null) {
-                    mPlugin.onTouchTraveled(getDistancePx());
+                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:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onTouchEnd(getDistancePx());
+                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_DELEGATE_ACTIVE) {
-            mGestureDetector.onTouchEvent(ev);
-        }
-
         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 instanceof LauncherRecentsView)
-                && ((LauncherRecentsView) mRecentsView).hasRecentsExtraCard()) {
+        if (mRecentsView != null && mRecentsView.hasRecentsExtraCard()) {
             maxIndex = 1;
         }
 
-        boolean atRightMostApp = (mRecentsView == null
-                || mRecentsView.getRunningTaskIndex() <= maxIndex);
+        boolean atRightMostApp = mRecentsView == null
+                || (mRecentsView.getRunningTaskIndex() <= maxIndex);
 
         // Check if the gesture is within our angle threshold of horizontal
-        float deltaY = Math.abs(mLastPos.y - mDownPos.y);
-        float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
-        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
+        float deltaY = abs(mLastPos.y - mDownPos.y);
+        float deltaX = mLastPos.x - mDownPos.x;
 
-        return atRightMostApp && angleInBounds;
+        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() {
@@ -225,35 +266,22 @@
         return deviceState;
     }
 
-    private int getDistancePx() {
-        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+    private int getHorizontalDistancePx() {
+        return (int) (mLastPos.x - mDownPos.x);
     }
 
-    private String getUnderlyingActivity() {
+    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();
     }
-
-    private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-            if (isValidAngle(velocityX, -velocityY)
-                    && getDistancePx() >= mFlingThresholdPx
-                    && mState != STATE_DELEGATE_ACTIVE) {
-
-                if (mPlugin != null) {
-                    mPlugin.onFling(-velocityX);
-                }
-            }
-            return true;
-        }
-
-        private boolean isValidAngle(float deltaX, float deltaY) {
-            float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
-            // normalize so that angle is measured clockwise from horizontal in the bottom right
-            // corner and counterclockwise from horizontal in the bottom left corner
-
-            angle = angle > 90 ? 180 - angle : angle;
-            return (angle < mAngleThreshold);
-        }
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index d3160b3..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
@@ -18,13 +18,14 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
 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;
@@ -35,22 +36,18 @@
 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 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;
@@ -63,15 +60,7 @@
         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
@@ -86,16 +75,15 @@
 
     @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);
 
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 ac1c3a8..d972c0f 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,15 +15,20 @@
  */
 package com.android.quickstep.inputconsumers;
 
+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 com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-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.GestureState;
@@ -33,19 +38,22 @@
 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 Context mContext;
     private final InputMonitorCompat mInputMonitor;
     private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+    private final GestureState mGestureState;
 
     public OverviewWithoutFocusInputConsumer(Context context,
             RecentsAnimationDeviceState deviceState, GestureState gestureState,
             InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
         mContext = context;
+        mGestureState = gestureState;
         mInputMonitor = inputMonitor;
         mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
-                deviceState.getNavBarPosition(), this::onInterceptTouch, this::onSwipeUp);
+                deviceState.getNavBarPosition(), this::onInterceptTouch, this);
     }
 
     @Override
@@ -70,7 +78,8 @@
         }
     }
 
-    private void onSwipeUp(boolean wasFling) {
+    @Override
+    public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
         mContext.startActivity(new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@@ -78,9 +87,23 @@
         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());
+        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/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 b9ef57e..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,8 +19,8 @@
 
 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;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
deleted file mode 100644
index d9e9cc7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ /dev/null
@@ -1,582 +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 android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Build;
-import android.view.Surface;
-
-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.model.PagedViewOrientedState;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RemoteAnimationTargets;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-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;
-
-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.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-/**
- * Utility class to handle window clip animation
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class AppWindowAnimationHelper {
-
-    // 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 device coordinates
-    private final RectF mSourceRect = new RectF();
-    // The bounds of the task view in device coordinates
-    private final RectF mTargetRect = new RectF();
-    // The bounds of the app window (between mSourceRect and mTargetRect) in device coordinates
-    private final RectF mCurrentRect = 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 clip rect in source app window coordinates. The app window surface will only be drawn
-    // within these bounds. This clip rect starts at the full mSourceStackBounds, and insets by
-    // mSourceWindowClipInsets as the transform progress goes to 1.
-    private final RectF mCurrentClipRectF = new RectF();
-
-    // The bounds of launcher (not including insets) in device coordinates
-    public final Rect mHomeStackBounds = new Rect();
-    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();
-    private PagedViewOrientedState mOrientedState;
-    // 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 AppWindowAnimationHelper(PagedViewOrientedState orientedState, Context context) {
-        Resources res = context.getResources();
-        mOrientedState = orientedState;
-        mWindowCornerRadius = getWindowCornerRadius(res);
-        mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(res);
-        mTaskCornerRadius = TaskCornerRadius.get(context);
-        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
-    }
-
-    public AppWindowAnimationHelper(Context context) {
-        this(null, context);
-    }
-
-    private void updateSourceStack(RemoteAnimationTargetCompat target) {
-        mSourceInsets.set(target.contentInsets);
-        mSourceStackBounds.set(target.screenSpaceBounds);
-
-        // 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);
-        float scale = getSrcToTargetScale();
-        Utilities.scaleRectFAboutCenter(scaledTargetRect, scale);
-
-        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));
-        mSourceRect.set(scaledTargetRect);
-    }
-
-    private float getSrcToTargetScale() {
-        if (mOrientedState == null ||
-            (mOrientedState.getDisplayRotation() == Surface.ROTATION_0
-                || mOrientedState.getDisplayRotation() == Surface.ROTATION_180)) {
-            return mSourceRect.width() / mTargetRect.width();
-        } else {
-            return mSourceRect.height() / mTargetRect.height();
-        }
-    }
-
-    public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
-        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
-        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
-    }
-
-    public RectF applyTransform(TransformParams params) {
-        SurfaceParams[] surfaceParams = computeSurfaceParams(params);
-        if (surfaceParams == null) {
-            return null;
-        }
-        applySurfaceParams(params.mSyncTransactionApplier, surfaceParams);
-        return mCurrentRect;
-    }
-
-    /**
-     * Updates this AppWindowAnimationHelper's state based on the given TransformParams, and returns
-     * the SurfaceParams to apply via {@link SyncRtSurfaceTransactionApplierCompat#applyParams}.
-     */
-    public SurfaceParams[] computeSurfaceParams(TransformParams params) {
-        if (params.mTargetSet == null) {
-            return null;
-        }
-
-        float progress = Utilities.boundToRange(params.mProgress, 0, 1);
-        updateCurrentRect(params);
-
-        SurfaceParams[] surfaceParams = new SurfaceParams[params.mTargetSet.unfilteredApps.length];
-        for (int i = 0; i < params.mTargetSet.unfilteredApps.length; i++) {
-            RemoteAnimationTargetCompat app = params.mTargetSet.unfilteredApps[i];
-            SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash);
-            if (app.localBounds != null) {
-                mTmpMatrix.setTranslate(0, 0);
-                if (app.activityType == ACTIVITY_TYPE_HOME && app.mode == MODE_CLOSING) {
-                    mTmpMatrix.setTranslate(app.localBounds.left, app.localBounds.top);
-                }
-            } else {
-                mTmpMatrix.setTranslate(app.position.x, app.position.y);
-            }
-
-            Rect crop = mTmpRect;
-            crop.set(app.screenSpaceBounds);
-            crop.offsetTo(0, 0);
-            float alpha;
-            float cornerRadius = 0f;
-            float scale = Math.max(mCurrentRect.width(), mTargetRect.width()) / crop.width();
-            int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
-            if (app.mode == params.mTargetSet.targetMode) {
-                alpha = mTaskAlphaCallback.getAlpha(app, params.mTargetAlpha);
-                if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                    mTmpMatrix.setRectToRect(mSourceRect, mCurrentRect, ScaleToFit.FILL);
-                    if (app.localBounds != null) {
-                        mTmpMatrix.postTranslate(app.localBounds.left, app.localBounds.top);
-                    } else {
-                        mTmpMatrix.postTranslate(app.position.x, app.position.y);
-                    }
-                    mCurrentClipRectF.roundOut(crop);
-                    if (mSupportsRoundedCornersOnWindows) {
-                        if (params.mCornerRadius > -1) {
-                            cornerRadius = params.mCornerRadius;
-                            scale = mCurrentRect.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 (params.mTargetSet.hasRecents) {
-                    // If home has a different target then recents, reverse anim the
-                    // home target.
-                    alpha = 1 - (progress * params.mTargetAlpha);
-                }
-            } else {
-                alpha = mBaseAlphaCallback.getAlpha(app, progress);
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.mLauncherOnTop) {
-                    crop = null;
-                    layer = Integer.MAX_VALUE;
-                }
-            }
-            builder.withAlpha(alpha)
-                    .withMatrix(mTmpMatrix)
-                    .withWindowCrop(crop)
-                    .withLayer(layer)
-                    // Since radius is in Surface space, but we draw the rounded corners in screen
-                    // space, we have to undo the scale
-                    .withCornerRadius(cornerRadius / scale);
-            surfaceParams[i] = builder.build();
-        }
-        return surfaceParams;
-    }
-
-    public RectF updateCurrentRect(TransformParams params) {
-        if (params.mCurrentRect != null) {
-            mCurrentRect.set(params.mCurrentRect);
-        } else {
-            mTmpRectF.set(mTargetRect);
-            Utilities.scaleRectFAboutCenter(mTmpRectF, params.mOffsetScale);
-            mCurrentRect.set(mRectFEvaluator.evaluate(params.mProgress, mSourceRect, mTmpRectF));
-            if (mOrientedState == null || mOrientedState.areMultipleLayoutOrientationsDisabled()) {
-                mCurrentRect.offset(params.mOffset, 0);
-            } else {
-                int displayRotation = mOrientedState.getDisplayRotation();
-                mOrientedState.getOrientationHandler().offsetTaskRect(mCurrentRect,
-                    params.mOffset, displayRotation);
-            }
-        }
-
-        updateClipRect(params);
-        return mCurrentRect;
-    }
-
-    private void updateClipRect(TransformParams params) {
-        // Don't clip past progress > 1.
-        float progress = Math.min(1, params.mProgress);
-        mCurrentClipRectF.left = mSourceWindowClipInsets.left * progress;
-        mCurrentClipRectF.top = mSourceWindowClipInsets.top * progress;
-        mCurrentClipRectF.right =
-                mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress);
-        mCurrentClipRectF.bottom =
-                mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress);
-    }
-
-    public RectF getCurrentRectWithInsets() {
-        mTmpMatrix.mapRect(mCurrentRectWithInsets, mCurrentClipRectF);
-        return mCurrentRectWithInsets;
-    }
-
-    public static 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 AppWindowAnimationHelper 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 = getSrcToTargetScale();
-        float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
-        return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
-    }
-
-    private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
-        SystemUiProxy proxy = SystemUiProxy.INSTANCE.get(activity);
-        if (proxy.isActive()) {
-            mSourceStackBounds.set(proxy.getNonMinimizedSplitScreenSecondaryBounds());
-            return;
-        }
-
-        // 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 {
-        private float mProgress;
-        private float mOffset;
-        private float mOffsetScale;
-        private @Nullable RectF mCurrentRect;
-        private float mTargetAlpha;
-        private float mCornerRadius;
-        private boolean mLauncherOnTop;
-        private RemoteAnimationTargets mTargetSet;
-        private SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
-
-        public TransformParams() {
-            mProgress = 0;
-            mOffset = 0;
-            mOffsetScale = 1;
-            mCurrentRect = null;
-            mTargetAlpha = 1;
-            mCornerRadius = -1;
-            mLauncherOnTop = false;
-        }
-
-        /**
-         * 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;
-        }
-
-        /**
-         * Sets the current rect to show the transformed window, in device coordinates. This gives
-         * the caller manual control of where to show the window. If unspecified (null), we
-         * interpolate between {@link AppWindowAnimationHelper#mSourceRect} and
-         * {@link AppWindowAnimationHelper#mTargetRect}, based on {@link #mProgress}.
-         */
-        public TransformParams setCurrentRect(RectF currentRect) {
-            mCurrentRect = currentRect;
-            return this;
-        }
-
-        /**
-         * Specifies the alpha of the transformed window. Default is 1.
-         */
-        public TransformParams setTargetAlpha(float targetAlpha) {
-            mTargetAlpha = targetAlpha;
-            return this;
-        }
-
-        /**
-         * If {@link #mCurrentRect} is null (i.e. {@link #setCurrentRect(RectF)} hasn't overridden
-         * the default), then offset the current rect by this amount after computing the rect based
-         * on {@link #mProgress}.
-         */
-        public TransformParams setOffset(float offset) {
-            mOffset = offset;
-            return this;
-        }
-
-        /**
-         * If {@link #mCurrentRect} is null (i.e. {@link #setCurrentRect(RectF)} hasn't overridden
-         * the default), then scale the current rect by this amount after computing the rect based
-         * on {@link #mProgress}.
-         */
-        public TransformParams setOffsetScale(float offsetScale) {
-            mOffsetScale = offsetScale;
-            return this;
-        }
-
-        /**
-         * If true, sets the crop = null and layer = Integer.MAX_VALUE for targets that don't match
-         * {@link #mTargetSet}.targetMode. (Currently only does this when live tiles are enabled.)
-         */
-        public TransformParams setLauncherOnTop(boolean launcherOnTop) {
-            mLauncherOnTop = launcherOnTop;
-            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(
-                SyncRtSurfaceTransactionApplierCompat applier) {
-            mSyncTransactionApplier = applier;
-            return this;
-        }
-
-        // Pubic getters so outside packages can read the values.
-
-        public float getProgress() {
-            return mProgress;
-        }
-
-        public float getOffset() {
-            return mOffset;
-        }
-
-        public float getOffsetScale() {
-            return mOffsetScale;
-        }
-
-        @Nullable
-        public RectF getCurrentRect() {
-            return mCurrentRect;
-        }
-
-        public float getTargetAlpha() {
-            return mTargetAlpha;
-        }
-
-        public float getCornerRadius() {
-            return mCornerRadius;
-        }
-
-        public boolean isLauncherOnTop() {
-            return mLauncherOnTop;
-        }
-
-        public RemoteAnimationTargets getTargetSet() {
-            return mTargetSet;
-        }
-
-        public SyncRtSurfaceTransactionApplierCompat getSyncTransactionApplier() {
-            return mSyncTransactionApplier;
-        }
-    }
-}
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 dde7605..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
@@ -29,6 +29,7 @@
 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;
@@ -39,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") {
@@ -116,6 +117,7 @@
         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() {
@@ -190,10 +192,12 @@
                     maybeOnEnd();
                 });
 
+        setCanRelease(false);
+        mAnimsStarted = true;
+
         mRectXAnim.start();
         mRectYAnim.start();
         mRectScaleAnim.start();
-        mAnimsStarted = true;
         for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
             animatorListener.onAnimationStart(null);
         }
@@ -207,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());
@@ -229,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 217eca5..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,10 +15,10 @@
  */
 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;
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 9cf45b3..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
@@ -20,16 +20,20 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 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.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;
@@ -38,8 +42,10 @@
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PropertySetter;
 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;
@@ -63,7 +69,7 @@
     private final AnimatorSet mAnimators = new AnimatorSet();
 
     public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim) {
-        prepareToAnimate(launcher);
+        prepareToAnimate(launcher, animateOverviewScrim);
 
         mVelocity = velocity;
 
@@ -117,15 +123,21 @@
                 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());
         }
 
+        addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+
         mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
                 .setDuration(ALPHA_DURATION_MS));
         mAnimators.addListener(new AnimatorListenerAdapter() {
@@ -144,21 +156,30 @@
     /**
      * Setup workspace with 0 duration to prepare for our staggered animation.
      */
-    private void prepareToAnimate(Launcher launcher) {
+    private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
         StateAnimationConfig config = new StateAnimationConfig();
-        config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW;
+        config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
         config.duration = 0;
         // setRecentsAttachedToAppWindow() will animate recents out.
         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.
      */
@@ -184,14 +205,21 @@
         ResourceProvider rp = DynamicResource.provider(v.getContext());
         float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
         float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
-        ObjectAnimator springTransY = new SpringAnimationBuilder<>(v, VIEW_TRANSLATE_Y)
+        ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
                 .setStiffness(stiffness)
                 .setDampingRatio(damping)
                 .setMinimumVisibleChange(1f)
+                .setStartValue(mSpringTransY)
                 .setEndValue(0)
                 .setStartVelocity(mVelocity)
-                .build(v.getContext());
+                .build(v, VIEW_TRANSLATE_Y);
         springTransY.setStartDelay(startDelay);
+        springTransY.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                v.setTranslationY(0f);
+            }
+        });
         mAnimators.play(springTransY);
 
         v.setAlpha(0);
@@ -199,16 +227,32 @@
         alpha.setInterpolator(LINEAR);
         alpha.setDuration(ALPHA_DURATION_MS);
         alpha.setStartDelay(startDelay);
+        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) {
-        PendingAnimation builder = new PendingAnimation(duration, mAnimators);
-        launcher.getWorkspace().getStateTransitionAnimation().setScrim(builder, state);
-        builder.setFloat(
+    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/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
index c71258b..29b9558 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -149,8 +149,12 @@
             isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
         }
 
-        if (isSwipeUp && mOnSwipeUp != null) {
-            mOnSwipeUp.onSwipeUp(wasFling);
+        if (mOnSwipeUp != null) {
+            if (isSwipeUp) {
+                mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
+            } else {
+                mOnSwipeUp.onSwipeUpCancelled();
+            }
         }
     }
 
@@ -161,7 +165,11 @@
         /**
          * 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);
+        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 763f5be..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
@@ -47,23 +47,27 @@
     private boolean mIsRtl;
 
     private int mScrollOffset;
-    private RecentsView mParent;
 
     public ClearAllButton(Context context, AttributeSet attrs) {
         super(context, attrs);
+        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);
-        mScrollOffset = mIsRtl ? mParent.getPaddingRight() / 2 : - mParent.getPaddingLeft() / 2;
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        mScrollOffset = orientationHandler.getClearAllScrollOffset(getRecentsView(), mIsRtl);
+    }
+
+    private RecentsView getRecentsView() {
+        return (RecentsView) getParent();
     }
 
     @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mParent = (RecentsView) getParent();
-        mIsRtl = !mParent.getPagedOrientationHandler().getRecentsRtlSetting(getResources());
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
     }
 
     @Override
@@ -78,21 +82,6 @@
         }
     }
 
-    public void onLayoutChanged() {
-        if (mParent == null) {
-            return;
-        }
-        setRotation(mParent.getPagedOrientationHandler().getDegreesRotated());
-    }
-
-    public void setRtl(boolean rtl) {
-        if (mIsRtl == rtl) {
-            return;
-        }
-        mIsRtl = rtl;
-        invalidate();
-    }
-
     public void setVisibilityAlpha(float alpha) {
         if (mVisibilityAlpha != alpha) {
             mVisibilityAlpha = alpha;
@@ -102,7 +91,7 @@
 
     @Override
     public void onPageScroll(ScrollState scrollState) {
-        PagedOrientationHandler orientationHandler = mParent.getPagedOrientationHandler();
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
         float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
         if (orientationSize == 0) {
             return;
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 98eb29a..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
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
@@ -29,43 +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.BaseQuickstepLauncher;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.states.RotationHelper;
+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.AppWindowAnimationHelper;
-import com.android.quickstep.util.AppWindowAnimationHelper.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<BaseQuickstepLauncher>
-        implements StateListener {
-
-    private static final Rect sTempRect = new Rect();
+        implements StateListener<LauncherState> {
 
     private final TransformParams mTransformParams = new TransformParams();
 
@@ -88,10 +84,6 @@
         }
     };
 
-    private RotationHelper.ForcedRotationChangedListener mForcedRotationChangedListener =
-            isForcedRotation -> LauncherRecentsView.this
-                    .disableMultipleLayoutRotations(!isForcedRotation);
-
     public LauncherRecentsView(Context context) {
         this(context, null);
     }
@@ -101,12 +93,17 @@
     }
 
     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() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             switchToScreenshot(null,
@@ -128,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,
-            AppWindowAnimationHelper 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,
@@ -179,39 +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) {
-        LauncherState.ScaleAndTranslation overviewScaleAndTranslation =
-            NORMAL.getOverviewScaleAndTranslation(mActivity);
-        float offscreen = mOrientationHandler.getTranslationValue(overviewScaleAndTranslation);
-        // Offset since scale pushes tasks outwards.
-        getTaskSize(sTempRect);
-        int taskSize = mOrientationHandler.getPrimarySize(sTempRect);
-        offscreen += taskSize * (recentsScale - 1) / 2;
-        if (mRunningTaskTileHidden) {
-            // The first task is hidden, so offset by its width.
-            offscreen -= (taskSize + getPageSpacing()) * recentsScale;
-        }
-        if (isRtl()) {
-            offscreen = -offscreen;
-        }
-        return offscreen;
-    }
-
-    @Override
     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (tv.isRunningTask()) {
                 mTransformParams.setProgress(1 - progress)
-                        .setCurrentRect(null)
                         .setSyncTransactionApplier(mSyncTransactionApplier);
-                mAppWindowAnimationHelper.applyTransform(mTransformParams);
+                // TODO: Revisit live tiles
             } else {
                 redrawLiveTile(true);
             }
@@ -219,14 +170,21 @@
     }
 
     @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
@@ -243,18 +201,10 @@
     }
 
     @Override
-    public void redrawLiveTile(boolean mightNeedToRefill) {
-        AppWindowAnimationHelper.TransformParams transformParams = getLiveTileParams(mightNeedToRefill);
-        if (transformParams != null) {
-            mAppWindowAnimationHelper.applyTransform(transformParams);
-        }
-    }
-
-    @Override
-    public AppWindowAnimationHelper.TransformParams getLiveTileParams(
+    public TransformParams getLiveTileParams(
             boolean mightNeedToRefill) {
         if (!mEnableDrawingLiveTile || mRecentsAnimationController == null
-                || mRecentsAnimationTargets == null || mAppWindowAnimationHelper == null) {
+                || mRecentsAnimationTargets == null) {
             return null;
         }
         TaskView taskView = getRunningTaskView();
@@ -274,27 +224,19 @@
             if (mightNeedToRefill && offsetY > 0) {
                 mTempRect.top -= offsetY;
             }
-            mTempRectF.set(mTempRect);
             mTransformParams.setProgress(1f)
-                    .setCurrentRect(mTempRectF)
                     .setTargetAlpha(taskView.getAlpha())
                     .setSyncTransactionApplier(mSyncTransactionApplier)
-                    .setTargetSet(mRecentsAnimationTargets)
-                    .setLauncherOnTop(true);
+                    .setTargetSet(mRecentsAnimationTargets);
         }
         return mTransformParams;
     }
 
     @Override
-    protected boolean supportsVerticalLandscape() {
-        return FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()
-                && !mOrientationState.areMultipleLayoutOrientationsDisabled();
-    }
-
-    @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);
@@ -312,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);
     }
 
@@ -344,7 +286,6 @@
         super.onAttachedToWindow();
         PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
                 mRecentsExtraCardPluginListener, RecentsExtraCard.class);
-        mActivity.getRotationHelper().addForcedRotationCallback(mForcedRotationChangedListener);
     }
 
     @Override
@@ -352,7 +293,6 @@
         super.onDetachedFromWindow();
         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(
                 mRecentsExtraCardPluginListener);
-        mActivity.getRotationHelper().removeForcedRotationCallback(mForcedRotationChangedListener);
     }
 
     @Override
@@ -412,4 +352,16 @@
     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/OverviewActionsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
index 6a37e2b..a2da398 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
@@ -16,34 +16,81 @@
 
 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.LayoutInflater;
 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 extends FrameLayout {
+public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
+        implements OnClickListener, Insettable {
 
-    private final View mScreenshotButton;
-    private final View mShareButton;
+    private final Rect mInsets = new Rect();
 
-    /**
-     * Listener for taps on the various actions.
-     */
-    public interface Listener {
-        /** User has initiated the share actions. */
-        void onShare();
+    @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 { }
 
-        /** User has initiated the screenshot action. */
-        void onScreenshot();
-    }
+    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);
@@ -54,26 +101,121 @@
     }
 
     public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
+        super(context, attrs, defStyleAttr, 0);
+        mMultiValueAlpha = new MultiValueAlpha(this, 4);
     }
 
-    public OverviewActionsView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        LayoutInflater.from(context).inflate(R.layout.overview_actions, this, true);
-        mShareButton = findViewById(R.id.action_share);
-        mScreenshotButton = findViewById(R.id.action_screenshot);
+    @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 listener for callbacks, or {@code null} to clear the listener.
+     * @param callbacks for callbacks, or {@code null} to clear the listener.
      */
-    public void setListener(@Nullable OverviewActionsView.Listener listener) {
-        mShareButton.setOnClickListener(
-                listener == null ? null : view -> listener.onShare());
-        mScreenshotButton.setOnClickListener(
-                listener == null ? null : view -> listener.onScreenshot());
+    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 e3b3102..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,22 +16,27 @@
 
 package com.android.quickstep.views;
 
+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_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_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.TASK_DISMISS_SWIPE_UP;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.TASK_LAUNCH_SWIPE_DOWN;
+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;
@@ -39,6 +44,10 @@
 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.AnimatorSet;
 import android.animation.LayoutTransition;
@@ -50,10 +59,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
 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;
@@ -66,12 +76,10 @@
 import android.util.FloatProperty;
 import android.util.Property;
 import android.util.SparseBooleanArray;
-import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.OrientationEventListener;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -85,9 +93,7 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
-import com.android.launcher3.InsettableFrameLayout;
 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;
@@ -100,28 +106,32 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.states.RotationHelper;
-import com.android.launcher3.testing.TestProtocol;
+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.Themes;
 import com.android.launcher3.util.ViewPool;
+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.TaskVisualsChangeListener;
-import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
-import com.android.quickstep.util.AppWindowAnimationHelper;
 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;
@@ -129,7 +139,6 @@
 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;
@@ -139,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, TaskVisualsChangeListener {
+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();
 
@@ -171,17 +181,45 @@
                 }
             };
 
-    private OrientationEventListener mOrientationListener;
-    private int mPreviousRotation;
+    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 AppWindowAnimationHelper mAppWindowAnimationHelper;
-    protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
+    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;
@@ -192,11 +230,9 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
-    private final int mTaskBottomMargin;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
-    protected final AppWindowAnimationHelper mTempAppWindowAnimationHelper;
 
     private final ScrollState mScrollState = new ScrollState();
     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
@@ -211,6 +247,8 @@
     private boolean mOverlayEnabled;
     protected boolean mFreezeViewVisibility;
 
+    private float mAdjacentPageOffset = 0;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -308,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;
@@ -325,29 +369,37 @@
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
-    private View mActionsView;
-    private boolean mGestureRunning = false;
+    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);
-        mTempAppWindowAnimationHelper =
-            new AppWindowAnimationHelper(getPagedViewOrientedState(), context);
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -359,7 +411,6 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
-        mTaskBottomMargin = LayoutUtils.thumbnailBottomMargin(context);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -375,26 +426,10 @@
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
         updateEmptyMessage();
-        disableMultipleLayoutRotations(!supportsVerticalLandscape());
+        mOrientationHandler = mOrientationState.getOrientationHandler();
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
-
-        mOrientationListener = new OrientationEventListener(getContext()) {
-            @Override
-            public void onOrientationChanged(int i) {
-                int rotation = RotationHelper.getRotationFromDegrees(i);
-                if (mPreviousRotation != rotation) {
-                    animateRecentsRotationInPlace(rotation);
-                    if (rotation == 0) {
-                        showActionsView();
-                    } else {
-                        hideActionsView();
-                    }
-                    mPreviousRotation = rotation;
-                }
-            }
-        };
     }
 
     public OverScroller getScroller() {
@@ -462,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
@@ -472,13 +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);
-        setActionsView();
+        mOrientationState.initListeners();
+        SplitScreenBounds.INSTANCE.addOnChangeListener(this);
     }
 
     @Override
@@ -492,7 +534,9 @@
         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
@@ -504,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++;
+            }
         }
     }
 
@@ -523,17 +599,9 @@
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
-        if (supportsVerticalLandscape()
-                && !TestProtocol.sDisableSensorRotation // Ignore hardware dependency for tests
-                && mOrientationListener.canDetectOrientation()) {
-            if (enabled) {
-                mOrientationListener.enable();
-            } else {
-                mOrientationListener.disable();
-            }
-        }
         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
@@ -550,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);
         }
@@ -584,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 {
@@ -612,7 +700,7 @@
     @Override
     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
         // Enables swiping to the left or right only if the task overlay is not modal.
-        if (getCurrentPageTaskView() == null || !getCurrentPageTaskView().isTaskOverlayModal()) {
+        if (!isModal()) {
             super.determineScrollingStart(ev, touchSlopScale);
         }
     }
@@ -642,7 +730,6 @@
         if (getTaskViewCount() != requiredTaskCount) {
             if (indexOfChild(mClearAllButton) != -1) {
                 removeView(mClearAllButton);
-                hideActionsView();
             }
             for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
                 addView(mTaskViewPool.getView());
@@ -652,7 +739,6 @@
             }
             if (requiredTaskCount > 0) {
                 addView(mClearAllButton);
-                showActionsView();
             }
         }
 
@@ -661,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, mLayoutRotation);
+            taskView.bind(task, mOrientationState);
         }
 
         if (mNextPage == INVALID_PAGE) {
@@ -685,6 +771,10 @@
         updateEnabledOverlays();
     }
 
+    private boolean isModal() {
+        return mTaskModalness > 0;
+    }
+
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             removeView(getTaskViewAt(i));
@@ -692,7 +782,6 @@
         if (indexOfChild(mClearAllButton) != -1) {
             removeView(mClearAllButton);
         }
-        hideActionsView();
     }
 
     public int getTaskViewCount() {
@@ -703,29 +792,19 @@
         return taskViewCount;
     }
 
-    /**
-     * Updates UI for a modal task, including hiding other tasks.
-     */
-    public void updateUiForModalTask(TaskView taskView, boolean isTaskOverlayModal) {
-        int currentIndex = indexOfChild(taskView);
-        TaskView previousTask = getTaskViewAt(currentIndex - 1);
-        TaskView nextTask = getTaskViewAt(currentIndex + 1);
-        if (previousTask != null) {
-            previousTask.setVisibility(isTaskOverlayModal ? View.INVISIBLE : View.VISIBLE);
-        }
-        if (nextTask != null) {
-            nextTask.setVisibility(isTaskOverlayModal ? View.INVISIBLE : View.VISIBLE);
-        }
+    protected void onTaskStackUpdated() {
+        // Lazily update the empty message only when the task stack is reapplied
+        updateEmptyMessage();
     }
 
-    protected void onTaskStackUpdated() { }
-
     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) {
@@ -740,6 +819,7 @@
         updateCurveProperties();
         // Update the set of visible task's data
         loadVisibleTaskData();
+        setTaskModalness(0);
     }
 
     public void setFullscreenProgress(float fullscreenProgress) {
@@ -748,9 +828,9 @@
         for (int i = 0; i < taskCount; i++) {
             getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
-        if (mActionsView != null) {
-            mActionsView.setVisibility(fullscreenProgress == 0 ? VISIBLE : INVISIBLE);
-        }
+        // Fade out the actions view quickly (0.1 range)
+        mActionsView.getFullscreenAlpha().setValue(
+                mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
     }
 
     private void updateTaskStackListenerState() {
@@ -767,22 +847,29 @@
     @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();
 
         mTempRect.top -= mTaskTopMargin;
-        mTempRect.bottom += mTaskBottomMargin;
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 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
@@ -819,7 +906,8 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(mOrientationHandler.getChildStart(page), mPageSpacing);
+            mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
+                    mPageSpacing);
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
@@ -909,12 +997,15 @@
 
         mRecentsAnimationController = null;
         mRecentsAnimationTargets = null;
-        mAppWindowAnimationHelper = 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() {
@@ -922,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);
     }
 
@@ -944,12 +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);
-        mGestureRunning = true;
+        mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, true);
     }
 
     /**
@@ -967,13 +1070,13 @@
     }
 
     private void animateRecentsRotationInPlace(int newRotation) {
-        if (!supportsVerticalLandscape()) {
+        if (mOrientationState.canRecentsActivityRotate()) {
+            // Let system take care of the rotation
             return;
         }
-
         AnimatorSet pa = setRecentsChangedOrientation(true);
         pa.addListener(AnimationSuccessListener.forRunnable(() -> {
-            updateLayoutRotation(newRotation);
+            setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
             mActivity.getDragLayer().recreateControllers();
             rotateAllChildTasks();
             setRecentsChangedOrientation(false).start();
@@ -995,12 +1098,10 @@
         return as;
     }
 
-    abstract protected boolean supportsVerticalLandscape();
 
     private void rotateAllChildTasks() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
-            taskView.setOverviewRotation(mLayoutRotation);
+            getTaskViewAt(i).setOrientationState(mOrientationState);
         }
     }
 
@@ -1008,15 +1109,26 @@
      * 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);
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             setRunningTaskViewShowScreenshot(true);
         }
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
-        mGestureRunning = false;
+        animateActionsViewIn();
+    }
+
+    /**
+     * Returns true if we should add a dummy taskView for the running task id
+     */
+    protected boolean shouldAddDummyTaskView(int runningTaskId) {
+        return getTaskView(runningTaskId) == null;
     }
 
     /**
@@ -1026,14 +1138,13 @@
      * 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);
             if (wasEmpty) {
                 addView(mClearAllButton);
-                showActionsView();
             }
             // The temporary running task is only used for the duration between the start of the
             // gesture and the task list is loaded and applied
@@ -1041,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, mLayoutRotation);
+            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;
@@ -1130,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);
     }
@@ -1207,7 +1332,7 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(int childStart, int pageSpacing) {
+        public void updateInterpolation(float childStart, int pageSpacing) {
             float pageCenter = childStart + halfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
             float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
@@ -1250,7 +1375,8 @@
             ComponentKey compKey = TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key);
             mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
                     endState.logAction, Direction.UP, index, compKey);
-            mActivity.getStatsLogManager().log(TASK_DISMISS_SWIPE_UP, taskView.buildProto());
+            mActivity.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo())
+                    .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
         }
     }
 
@@ -1357,7 +1483,6 @@
 
                     if (getTaskViewCount() == 0) {
                         removeViewInLayout(mClearAllButton);
-                        hideActionsView();
                         startHome();
                     } else {
                         snapToPageImmediately(pageToSnapTo);
@@ -1410,7 +1535,7 @@
         return true;
     }
 
-    private void runDismissAnimation(PendingAnimation pendingAnim) {
+    protected void runDismissAnimation(PendingAnimation pendingAnim) {
         AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
         controller.dispatchOnStart();
         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
@@ -1500,14 +1625,12 @@
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
         mEmptyIcon.setAlpha(alphaInt);
+        mActionsView.getContentAlpha().setValue(mContentAlpha);
+
         if (alpha > 0) {
             setVisibility(VISIBLE);
-            if (!mGestureRunning) {
-                showActionsView();
-            }
         } else if (!mFreezeViewVisibility) {
             setVisibility(GONE);
-            hideActionsView();
         }
     }
 
@@ -1518,34 +1641,61 @@
     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
         if (mFreezeViewVisibility != freezeViewVisibility) {
             mFreezeViewVisibility = freezeViewVisibility;
-
             if (!mFreezeViewVisibility) {
                 setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
-                if (mContentAlpha > 0) {
-                    showActionsView();
-                } else {
-                    hideActionsView();
-                }
             }
         }
     }
 
     @Override
-    public void setLayoutRotation(int touchRotation, int displayRotation) {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
-            return;
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (mActionsView != null) {
+            mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
         }
-
-        super.setLayoutRotation(touchRotation, displayRotation);
-        mClearAllButton.onLayoutChanged();
-        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
-        setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
     }
 
     @Override
-    public void onViewAdded(View child) {
-        super.onViewAdded(child);
-        child.setAlpha(mContentAlpha);
+    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
@@ -1554,11 +1704,6 @@
     }
 
     @Nullable
-    public TaskView getPreviousTaskView() {
-        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
-    }
-
-    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAtByAbsoluteIndex(getCurrentPage());
     }
@@ -1617,11 +1762,56 @@
 
         updateEmptyStateUi(changed);
 
-        // Set the pivot points to match the task preview center
-        setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
-                + (getHeight() - mInsets.bottom - getPaddingBottom() - mTaskBottomMargin)) / 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() {
@@ -1695,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, AppWindowAnimationHelper appWindowAnimationHelper) {
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
         AnimatorSet anim = new AnimatorSet();
 
         int taskIndex = indexOfChild(tv);
         int centerTaskIndex = getCurrentPage();
         boolean launchingCenterTask = taskIndex == centerTaskIndex;
 
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = appWindowAnimationHelper
-                .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.
@@ -1729,6 +1914,15 @@
         return anim;
     }
 
+    /**
+     * 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) {
@@ -1763,11 +1957,7 @@
             }
         });
 
-        AppWindowAnimationHelper appWindowAnimationHelper = new AppWindowAnimationHelper(
-            getPagedViewOrientedState(), mActivity);
-        appWindowAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this);
-        appWindowAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */);
-        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, appWindowAnimationHelper);
+        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
 
         DepthController depthController = getDepthController();
         if (depthController != null) {
@@ -1776,14 +1966,14 @@
             anim.play(depthAnimator);
         }
         anim.play(progressAnim);
-        anim.setDuration(duration).setInterpolator(interpolator);
+        anim.setInterpolator(interpolator);
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
         mPendingAnimation.addEndListener((endState) -> {
             if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
-                    onTaskLaunched(result);
+                    onTaskLaunchAnimationEnd(result);
                     if (!result) {
                         tv.notifyTaskLaunchFailed(TAG);
                     }
@@ -1794,11 +1984,11 @@
                     mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
                             endState.logAction, Direction.DOWN, indexOfChild(tv),
                             TaskUtils.getLaunchComponentKeyForTask(task.key));
-                    mActivity.getStatsLogManager().log(TASK_LAUNCH_SWIPE_DOWN, tv.buildProto()
-                    );
+                    mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
+                            .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
                 }
             } else {
-                onTaskLaunched(false);
+                onTaskLaunchAnimationEnd(false);
             }
             mPendingAnimation = null;
         });
@@ -1810,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);
@@ -1885,11 +2080,6 @@
         mRecentsAnimationTargets = recentsAnimationTargets;
     }
 
-    // TODO: To be removed in a follow up CL
-    public void setAppWindowAnimationHelper(AppWindowAnimationHelper appWindowAnimationHelper) {
-        mAppWindowAnimationHelper = appWindowAnimationHelper;
-    }
-
     public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
         mLiveTileOverlayAttached = liveTileOverlayAttached;
     }
@@ -1970,24 +2160,34 @@
         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 - mOrientationHandler.getPrimaryScroll(this);
-        offsetX *= mOrientationHandler.getPrimaryScale(this);
-        return offsetX;
+        return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this);
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
-            degreesRotated = mOrientationState.areMultipleLayoutOrientationsDisabled() ? 0 :
-                    RotationHelper.getDegreesFromRotation(mLayoutRotation);
+            degreesRotated = mOrientationHandler.getDegreesRotated();
         } else {
             degreesRotated = -navbarRotation;
         }
@@ -2000,27 +2200,20 @@
         // PagedOrientationHandler
         return e -> {
             if (navbarRotation != 0
-                    && !mOrientationState.areMultipleLayoutOrientationsDisabled()) {
-                RotationHelper.transformEventForNavBar(e, true);
+                    && mOrientationState.isMultipleOrientationSupportedByDevice()
+                    && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
+                mOrientationState.flipVertical(e);
                 super.onTouchEvent(e);
-                RotationHelper.transformEventForNavBar(e, false);
+                mOrientationState.flipVertical(e);
                 return;
             }
-            RotationHelper.transformEvent(-degreesRotated, e, true);
+            mOrientationState.transformEvent(-degreesRotated, e, true);
             super.onTouchEvent(e);
-            RotationHelper.transformEvent(-degreesRotated, e, false);
+            mOrientationState.transformEvent(-degreesRotated, e, false);
         };
     }
 
-    public AppWindowAnimationHelper getClipAnimationHelper() {
-        return mAppWindowAnimationHelper;
-    }
-
-    public AppWindowAnimationHelper getTempAppWindowAnimationHelper() {
-        return mTempAppWindowAnimationHelper;
-    }
-
-    public AppWindowAnimationHelper.TransformParams getLiveTileParams(
+    public TransformParams getLiveTileParams(
             boolean mightNeedToRefill) {
         return null;
     }
@@ -2056,25 +2249,20 @@
         }
     }
 
-    @Override
-    public void addView(View child, int index) {
-        // 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(
-                Utilities.isRtl(getResources())
-                        ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
-        super.addView(child, index);
-        if (isExtraCardView(child, index)) {
-            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);
         }
-    }
-
-    @Override
-    public void removeView(View view) {
-        if (isExtraCardView(view, indexOfChild(view))) {
-            mTaskViewStartIndex--;
-        }
-        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);
     }
 
     @Nullable
@@ -2082,9 +2270,21 @@
         return null;
     }
 
-    private boolean isExtraCardView(View view, int index) {
-        return !(view instanceof TaskView) && !(view instanceof ClearAllButton)
-                && index <= mTaskViewStartIndex;
+    @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;
     }
 
     /**
@@ -2113,34 +2313,4 @@
             mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
         }
     }
-
-    private void showActionsView() {
-        if (mActionsView != null && getTaskViewCount() > 0) {
-            mActionsView.setVisibility(VISIBLE);
-        }
-    }
-
-    private void hideActionsView() {
-        if (mActionsView != null) {
-            mActionsView.setVisibility(GONE);
-        }
-    }
-
-    private void setActionsView() {
-        if (mActionsView == null && ENABLE_OVERVIEW_ACTIONS.get()
-                && SysUINavigationMode.removeShelfFromOverview(mActivity)) {
-            mActionsView = ((ViewGroup) getParent()).findViewById(R.id.overview_actions_view);
-            if (mActionsView != null) {
-                Rect rect = new Rect();
-                getTaskSize(rect);
-                InsettableFrameLayout.LayoutParams layoutParams =
-                        new InsettableFrameLayout.LayoutParams(rect.width(),
-                                getResources().getDimensionPixelSize(
-                                        R.dimen.overview_actions_height));
-                layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
-                mActionsView.setLayoutParams(layoutParams);
-                showActionsView();
-            }
-        }
-    }
 }
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 80022b4..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
@@ -40,6 +40,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 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;
@@ -150,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) {
@@ -168,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);
@@ -200,21 +222,28 @@
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
+        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 178ff32..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
@@ -27,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;
@@ -34,23 +35,26 @@
 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.Log;
 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.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;
@@ -64,7 +68,9 @@
 
     private static final ColorMatrix COLOR_MATRIX = new ColorMatrix();
     private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
-    private static final RectF EMPTY_RECT_F = new RectF();
+
+    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 mIsOrientationChanged;
     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;
@@ -172,8 +180,7 @@
             mOverlay.reset();
         }
         if (mOverviewScreenshotActionsPlugin != null) {
-            mOverviewScreenshotActionsPlugin
-                .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
+            mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
         }
         updateThumbnailPaintFilter();
     }
@@ -193,11 +200,6 @@
         updateThumbnailPaintFilter();
     }
 
-    public void setSaturation(float saturation) {
-        mSaturation = saturation;
-        updateThumbnailPaintFilter();
-    }
-
     public TaskOverlay getTaskOverlay() {
         return mOverlay;
     }
@@ -213,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;
@@ -231,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,
@@ -270,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) {
@@ -294,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 {
@@ -323,9 +357,9 @@
     }
 
     private void updateOverlay() {
-        // The overlay doesn't really work when the screenshot is rotated, so don't add it.
-        if (mOverlayEnabled && !mIsOrientationChanged && 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();
         }
@@ -346,76 +380,21 @@
     }
 
     private void updateThumbnailMatrix() {
-        boolean isRotated = false;
-        boolean isOrientationDifferent = 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;
-
-            final float thumbnailScale;
-            int thumbnailRotation = mThumbnailData.rotation;
+            mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
+                    mThumbnailData.thumbnail.getHeight());
             int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    getResources().getConfiguration());
-            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
-            // Landscape vs portrait change
-            boolean windowingModeSupportsRotation = !mActivity.isInMultiWindowMode()
-                    && mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
-            isOrientationDifferent = isOrientationChange(deltaRotate)
-                && windowingModeSupportsRotation;
-            if (getMeasuredWidth() == 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.
+                    mActivity.getResources().getConfiguration());
+            mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
+                    getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
+                    currentRotation);
 
-                thumbnailScale = isOrientationDifferent
-                        ? getMeasuredWidth() / thumbnailHeight
-                        : getMeasuredWidth() / thumbnailWidth;
-            }
-
-            if (!isRotated) {
-                // No Rotation
-                mClippedInsets.offsetTo(thumbnailInsets.left * scale,
-                    thumbnailInsets.top * scale);
-                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
-            } else {
-                setThumbnailRotation(deltaRotate, thumbnailInsets, scale);
-            }
-
-            final float widthWithInsets;
-            final float heightWithInsets;
-            if (isOrientationDifferent) {
-                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((isOrientationDifferent ? thumbnailWidth : thumbnailHeight)
-                    * thumbnailScale, 0);
-            if (Math.round(bitmapHeight) < getMeasuredHeight()) {
-                mClipBottom = bitmapHeight;
-            }
+            mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
             mPaint.setShader(mBitmapShader);
         }
-
-        mIsOrientationChanged = isOrientationDifferent;
+        getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
         invalidate();
 
         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
@@ -423,51 +402,6 @@
         post(this::updateOverlay);
     }
 
-    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) {
-        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 = mThumbnailData.thumbnail.getHeight();
-                break;
-            case Surface.ROTATION_270:
-                newLeftInset = thumbnailInsets.top;
-                newTopInset = thumbnailInsets.right;
-                translateY = mThumbnailData.thumbnail.getWidth();
-                break;
-            case Surface.ROTATION_180:
-                newLeftInset = -thumbnailInsets.top;
-                newTopInset = -thumbnailInsets.left;
-                translateX = mThumbnailData.thumbnail.getWidth();
-                translateY = mThumbnailData.thumbnail.getHeight();
-                break;
-        }
-        mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
-        mMatrix.postTranslate(translateX - mClippedInsets.left,
-                translateY - mClippedInsets.top);
-    }
-
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
@@ -511,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 7010f9a..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
@@ -25,11 +25,14 @@
 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.TASK_LAUNCH_TAP;
+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;
@@ -38,13 +41,15 @@
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.content.Context;
+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.os.Process;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
@@ -56,15 +61,16 @@
 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.logger.LauncherAtom;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -78,10 +84,11 @@
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.LayoutUtils;
+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;
@@ -119,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
@@ -170,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;
@@ -187,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);
@@ -221,26 +215,30 @@
             mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
                     Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
                     TaskUtils.getLaunchComponentKeyForTask(getTask().key));
-            mActivity.getStatsLogManager().log(TASK_LAUNCH_TAP, buildProto());
+            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(getContext(), mCurrentFullscreenParams);
         setOutlineProvider(mOutlineProvider);
     }
 
-    /* Builds proto for logging */
-    protected LauncherAtom.ItemInfo buildProto() {
+    /**
+     * Builds proto for logging
+     */
+    public WorkspaceItemInfo getItemInfo() {
         ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key);
-        LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
-        itemBuilder.setIsWork(componentKey.user != Process.myUserHandle());
-        itemBuilder.setTask(LauncherAtom.Task.newBuilder()
-                .setComponentName(componentKey.componentName.flattenToShortString())
-                .setIndex(getRecentsView().indexOfChild(this)));
-        return itemBuilder.build();
+        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
@@ -248,31 +246,26 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
-        final Context context = getContext();
-
-        TaskView.LayoutParams thumbnailParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        thumbnailParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(context);
-        mSnapshotView.setLayoutParams(thumbnailParams);
     }
 
-    public boolean isTaskOverlayModal() {
-        return mSnapshotView.getTaskOverlay().isOverlayModal();
-    }
+    /**
+     * 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));
+        }
 
-    /** Updates UI based on whether the task is modal. */
-    public void updateUiForModalTask() {
-        boolean isOverlayModal = isTaskOverlayModal();
-        if (getRecentsView() != null) {
-            getRecentsView().updateUiForModalTask(this, isOverlayModal);
-        }
-        // Hide footers when overlay is modal.
-        if (isOverlayModal) {
-            for (FooterWrapper footer : mFooters) {
-                if (footer != null) {
-                    footer.animateHide();
-                }
-            }
-        }
+        updateFooterVerticalOffset(mFooterVerticalOffset);
     }
 
     public TaskMenuView getMenuView() {
@@ -285,12 +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, int recentsRotation) {
+    public void bind(Task task, RecentsOrientedState orientedState) {
         cancelPendingLoadTasks();
         mTask = task;
         mSnapshotView.bind(task);
-        setOverviewRotation(recentsRotation);
+        setOrientationState(orientedState);
     }
 
     public Task getTask() {
@@ -389,6 +385,7 @@
                             }
                         }, resultCallbackHandler);
             }
+            getRecentsView().onTaskLaunched(mTask);
         }
     }
 
@@ -434,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;
     }
@@ -459,19 +461,18 @@
         }
     }
 
-    void setOverviewRotation(int iconRotation) {
-        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
-        boolean isRtl = orientationHandler.getRecentsRtlSetting(getResources());
+    public void setOrientationState(RecentsOrientedState orientationState) {
+        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        snapshotParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(getContext());
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
-        int rotation = RotationHelper.getDegreesFromRotation(iconRotation);
-        switch (iconRotation) {
+        switch (orientationHandler.getRotation()) {
             case Surface.ROTATION_90:
-                iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+                iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
                 iconParams.rightMargin = -thumbnailPadding;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                iconParams.leftMargin = 0;
+                iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
             case Surface.ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
@@ -481,18 +482,21 @@
             case Surface.ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.leftMargin = -thumbnailPadding;
-                iconParams.rightMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                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 =
-                    iconParams.bottomMargin = 0;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
                 break;
         }
-        mSnapshotView.setLayoutParams(snapshotParams);
         mIconView.setLayoutParams(iconParams);
-        mIconView.setRotation(rotation);
+        mIconView.setRotation(orientationHandler.getDegreesRotated());
+
+        if (mMenuView != null) {
+            mMenuView.onRotationChanged();
+        }
     }
 
     private void setIconAndDimTransitionProgress(float progress, boolean invert) {
@@ -509,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) {
@@ -548,7 +547,7 @@
         setIconAndDimTransitionProgress(iconScale, invert);
     }
 
-    private void resetViewTransforms() {
+    protected void resetViewTransforms() {
         setCurveScale(1);
         setTranslationX(0f);
         setTranslationY(0f);
@@ -557,11 +556,6 @@
         setIconScaleAndDim(1);
     }
 
-    public void resetVisualProperties() {
-        resetViewTransforms();
-        setFullscreenProgress(0);
-    }
-
     public void setStableAlpha(float parentAlpha) {
         mStableAlpha = parentAlpha;
         setAlpha(mStableAlpha);
@@ -579,11 +573,15 @@
 
     @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(curveScaleForCurveInterpolation);
 
@@ -595,7 +593,10 @@
         }
 
         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());
         }
@@ -652,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);
@@ -668,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();
@@ -684,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.
@@ -708,13 +772,11 @@
     private static final class TaskOutlineProvider extends ViewOutlineProvider {
 
         private final int mMarginTop;
-        private final int mMarginBottom;
         private FullscreenDrawParams mFullscreenParams;
 
         TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
             mMarginTop = context.getResources().getDimensionPixelSize(
                     R.dimen.task_thumbnail_top_margin);
-            mMarginBottom = LayoutUtils.thumbnailBottomMargin(context);
             mFullscreenParams = fullscreenParams;
         }
 
@@ -729,7 +791,7 @@
             outline.setRoundRect(0,
                     (int) (mMarginTop * scale),
                     (int) ((insets.left + view.getWidth() + insets.right) * scale),
-                    (int) ((insets.top + view.getHeight() + insets.bottom - mMarginBottom) * scale),
+                    (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
                     mFullscreenParams.mCurrentDrawnCornerRadius);
         }
     }
@@ -752,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) {
@@ -780,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);
@@ -803,22 +859,19 @@
             animator.setDuration(100);
             animator.start();
         }
+    }
 
-        void animateHide() {
-            ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f);
-            animator.addUpdateListener(anim -> {
-                mFooterVerticalOffset = anim.getAnimatedFraction();
-                updateFooterOffset();
-            });
-            animator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    removeView(mView);
-                }
-            });
-            animator.setDuration(100);
-            animator.start();
+    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
@@ -876,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) {
@@ -892,9 +949,6 @@
      */
     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);
@@ -902,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
@@ -931,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;
@@ -956,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/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
similarity index 91%
rename from quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
rename to quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
index cd30ef7..57423c2 100644
--- a/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
+++ b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
@@ -16,5 +16,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <corners android:radius="?android:attr/dialogCornerRadius"/>
-    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/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
index a5c57b4..18ad2cb 100644
--- a/quickstep/res/drawable/back_gesture.xml
+++ b/quickstep/res/drawable/back_gesture.xml
@@ -1,100 +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.
--->
 <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:aapt="http://schemas.android.com/aapt">
     <aapt:attr name="android:drawable">
         <vector
-            android:width="206dp"
-            android:height="435dp"
-            android:viewportWidth="206"
-            android:viewportHeight="435">
-            <group android:name="edgeGroup"
-                android:translateX="197"
-                android:translateY="0">
-                <path
-                    android:name="edge"
-                    android:fillAlpha="0"
-                    android:fillType="nonZero"
-                    android:fillColor="#1a73eb"
-                    android:pathData=" M0,0 h9 v435 h-9 z " />
-            </group>
-            <group
-                android:name="trailGroup"
-                android:translateX="226"
-                android:translateY="200">
-                <path
-                    android:name="trail"
-                    android:fillAlpha="1"
-                    android:fillType="nonZero"
-                    android:pathData=" M0,0 h55 v36 h-55 z ">
-                    <aapt:attr name="android:fillColor">
-                        <gradient
-                            android:startX="0"
-                            android:endX="55"
-                            android:type="linear">
-                            <item
-                                android:color="#991a73eb"
-                                android:offset="0" />
-                            <item
-                                android:color="#401a73eb"
-                                android:offset="0.5" />
-                            <item
-                                android:color="#001a73eb"
-                                android:offset="1" />
-                        </gradient>
-                    </aapt:attr>
-                </path>
-            </group>
+            android:width="412dp"
+            android:height="890dp"
+            android:viewportWidth="412"
+            android:viewportHeight="890">
             <group android:name="_R_G">
                 <group
-                    android:name="_R_G_L_0_G_T_1"
-                    android:rotation="11"
-                    android:scaleX="0.9"
-                    android:scaleY="0.9"
-                    android:translateX="309"
-                    android:translateY="422.5">
+                    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_0_G"
-                        android:translateX="-145"
-                        android:translateY="-208">
+                        android:name="_R_G_L_3_G"
+                        android:translateX="-51.449"
+                        android:translateY="-51.449">
                         <path
-                            android:name="_R_G_L_0_G_D_0_P_0"
+                            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=" M12.5 -47 C-7.93,-41.24 -3,-20.5 -1.5,-7 C0,6.5 2.5,22 9,39.5 C13.52,51.67 17.06,63.52 19,113 C21,164 53.5,243.5 53.5,243.5 C53.5,243.5 59,275.5 123.5,326 C188,376.5 283.5,236 290.5,199 C297.5,162 194.5,80 149,73 C103.5,66 90.5,57.5 77,50 C63.5,42.5 57,27 54.5,13.5 C52,0 43.5,-15 40,-25 C36.5,-35 32,-52.5 12.5,-47c " />
-                        <path
-                            android:name="_R_G_L_0_G_D_1_P_0"
-                            android:pathData=" M4.45 -34.66 C4.45,-34.66 10.5,-12.66 10.5,-12.66 C11.24,-9.98 13.98,-8.38 16.67,-9.04 C16.67,-9.04 29.72,-12.27 29.72,-12.27 C32.39,-12.93 34.05,-15.59 33.47,-18.28 C33.47,-18.28 32.11,-24.57 32.11,-24.57 "
-                            android:strokeWidth="4"
-                            android:strokeAlpha="1"
-                            android:strokeColor="#a0c2f9" />
-                        <path
-                            android:name="_R_G_L_0_G_D_2_P_0"
-                            android:pathData=" M18.35 21.81 C21.41,17.24 36.97,10.77 44.63,13.55 "
-                            android:strokeWidth="4"
-                            android:strokeAlpha="1"
-                            android:strokeColor="#a0c2f9" />
+                            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="edge">
+    <target android:name="_R_G_L_3_G_D_0_P_0">
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
@@ -102,44 +123,27 @@
                     android:propertyName="fillAlpha"
                     android:startOffset="0"
                     android:valueFrom="0"
-                    android:valueTo="0.2"
-                    android:valueType="floatType">
-                    <aapt:attr name="android:interpolator">
-                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
-                    </aapt:attr>
-                </objectAnimator>
-                <objectAnimator
-                    android:duration="917"
-                    android:propertyName="fillAlpha"
-                    android:startOffset="333"
-                    android:valueFrom="0.2"
-                    android:valueTo="0.2"
-                    android:valueType="floatType">
-                    <aapt:attr name="android:interpolator">
-                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
-                    </aapt:attr>
-                </objectAnimator>
-                <objectAnimator
-                    android:duration="583"
-                    android:propertyName="fillAlpha"
-                    android:startOffset="1250"
-                    android:valueFrom="0.2"
                     android:valueTo="0"
                     android:valueType="floatType">
                     <aapt:attr name="android:interpolator">
                         <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
-            </set>
-        </aapt:attr>
-    </target>
-    <target android:name="trail">
-        <aapt:attr name="android:animation">
-            <set android:ordering="together">
                 <objectAnimator
-                    android:duration="2000"
+                    android:duration="167"
                     android:propertyName="fillAlpha"
-                    android:startOffset="0"
+                    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">
@@ -148,9 +152,9 @@
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="850"
+                    android:duration="367"
                     android:propertyName="fillAlpha"
-                    android:startOffset="2000"
+                    android:startOffset="1000"
                     android:valueFrom="1"
                     android:valueTo="0"
                     android:valueType="floatType">
@@ -161,51 +165,468 @@
             </set>
         </aapt:attr>
     </target>
-    <target android:name="trailGroup">
+    <target android:name="_R_G_L_3_G_T_1">
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="83"
+                    android:duration="333"
                     android:propertyName="translateX"
-                    android:startOffset="1250"
-                    android:valueFrom="226"
-                    android:valueTo="226"
+                    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.167,0.167 0.285,1 1.0,1.0" />
+                        <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="translateX"
-                    android:startOffset="1333"
-                    android:valueFrom="226"
-                    android:valueTo="151"
-                    android:valueType="floatType">
+                    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.167 0.285,1 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="517"
-                    android:propertyName="translateX"
-                    android:startOffset="2333"
-                    android:valueFrom="151"
-                    android:valueTo="151"
-                    android:valueType="floatType">
+                    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.167 0.285,1 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="50"
-                    android:propertyName="translateX"
-                    android:startOffset="2850"
-                    android:valueFrom="226"
-                    android:valueTo="226"
+                    android: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.167,0.167 0.285,1 1.0,1.0" />
+                        <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>
@@ -215,25 +636,47 @@
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="1833"
-                    android:propertyName="fillAlpha"
-                    android:startOffset="1250"
-                    android:valueFrom="1"
-                    android:valueTo="1"
-                    android:valueType="floatType">
+                    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.167,0.167 0.833,0.833 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="167"
-                    android:propertyName="fillAlpha"
-                    android:startOffset="3083"
-                    android:valueFrom="1"
-                    android:valueTo="0"
-                    android:valueType="floatType">
+                    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.167,0.167 0.833,0.833 1.0,1.0" />
+                        <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>
@@ -243,109 +686,197 @@
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="1833"
-                    android:propertyName="strokeAlpha"
-                    android:startOffset="1250"
-                    android:valueFrom="1"
-                    android:valueTo="1"
-                    android:valueType="floatType">
+                    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.167,0.167 0.833,0.833 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="100"
-                    android:propertyName="strokeAlpha"
-                    android:startOffset="3083"
-                    android:valueFrom="1"
-                    android:valueTo="0"
-                    android:valueType="floatType">
+                    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.167,0.167 0.833,0.833 1.0,1.0" />
+                        <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_D_2_P_0">
+    <target android:name="_R_G_L_0_G_N_4_T_1">
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="1833"
-                    android:propertyName="strokeAlpha"
-                    android:startOffset="1250"
-                    android:valueFrom="1"
-                    android:valueTo="1"
-                    android:valueType="floatType">
-                    <aapt:attr name="android:interpolator">
-                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
-                    </aapt:attr>
-                </objectAnimator>
-                <objectAnimator
-                    android:duration="100"
-                    android:propertyName="strokeAlpha"
-                    android:startOffset="3083"
-                    android:valueFrom="1"
-                    android:valueTo="0"
-                    android:valueType="floatType">
-                    <aapt:attr name="android:interpolator">
-                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
-                    </aapt:attr>
-                </objectAnimator>
-            </set>
-        </aapt:attr>
-    </target>
-    <target android:name="_R_G_L_0_G_T_1">
-        <aapt:attr name="android:animation">
-            <set android:ordering="together">
-                <objectAnimator
-                    android:duration="83"
+                    android:duration="333"
                     android:propertyName="translateX"
-                    android:startOffset="1250"
-                    android:valueFrom="309"
-                    android:valueTo="309"
+                    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.167,0.167 0.285,1 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="1417"
+                    android:duration="667"
                     android:propertyName="translateX"
-                    android:startOffset="1333"
-                    android:valueFrom="309"
-                    android:valueTo="251"
+                    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.167,0.167 0.285,1 1.0,1.0" />
+                        <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_T_1">
+    <target android:name="_R_G_L_0_G_N_4_T_1">
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="83"
-                    android:propertyName="rotation"
-                    android:startOffset="1250"
-                    android:valueFrom="11"
-                    android:valueTo="11"
+                    android: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.167,0.167 0.277,1 1.0,1.0" />
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0" />
                     </aapt:attr>
                 </objectAnimator>
                 <objectAnimator
-                    android:duration="1417"
+                    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="1333"
-                    android:valueFrom="11"
+                    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.167,0.167 0.277,1 1.0,1.0" />
+                        <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>
@@ -355,9 +886,9 @@
         <aapt:attr name="android:animation">
             <set android:ordering="together">
                 <objectAnimator
-                    android:duration="2183"
+                    android:duration="2667"
                     android:propertyName="translateX"
-                    android:startOffset="1250"
+                    android:startOffset="0"
                     android:valueFrom="0"
                     android:valueTo="1"
                     android:valueType="floatType" />
diff --git a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
similarity index 91%
rename from quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
rename to quickstep/res/drawable/gesture_tutorial_action_button_background.xml
index d7b9102..3f3b288 100644
--- a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
+++ b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
@@ -16,5 +16,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <corners android:radius="@dimen/default_dialog_corner_radius"/>
-    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_close_button.xml b/quickstep/res/drawable/gesture_tutorial_close_button.xml
similarity index 100%
rename from quickstep/res/drawable/back_gesture_tutorial_close_button.xml
rename to quickstep/res/drawable/gesture_tutorial_close_button.xml
diff --git a/quickstep/res/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/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/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
deleted file mode 100644
index d8c25bd..0000000
--- a/quickstep/res/layout/back_gesture_tutorial_fragment.xml
+++ /dev/null
@@ -1,119 +0,0 @@
-<!--
-    Copyright (C) 2020 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/back_gesture_tutorial_background_color">
-
-    <ImageView
-        android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:scaleType="centerCrop"/>
-
-    <ImageButton
-        android:id="@+id/back_gesture_tutorial_fragment_close_button"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:padding="18dp"
-        android:layout_marginTop="30dp"
-        android:layout_marginStart="4dp"
-        android:layout_alignParentLeft="true"
-        android:layout_alignParentTop="true"
-        android:background="@android:color/transparent"
-        android:accessibilityTraversalAfter="@id/back_gesture_tutorial_fragment_titles_container"
-        android:contentDescription="@string/back_gesture_tutorial_close_button_content_description"
-        android:src="@drawable/back_gesture_tutorial_close_button"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="70dp"
-        android:orientation="vertical">
-
-        <LinearLayout
-            android:id="@+id/back_gesture_tutorial_fragment_titles_container"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:focusable="true">
-
-            <TextView
-                android:id="@+id/back_gesture_tutorial_fragment_title_view"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_title_margin_start_end"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_title_margin_start_end"
-                style="@style/TextAppearance.BackGestureTutorial.Title"/>
-
-            <TextView
-                android:id="@+id/back_gesture_tutorial_fragment_subtitle_view"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginTop="10dp"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
-                style="@style/TextAppearance.BackGestureTutorial.Subtitle"/>
-
-        </LinearLayout>
-
-        <Space
-            android:layout_width="wrap_content"
-            android:layout_weight="1"
-            android:layout_height="0dp"
-            android:layout_marginTop="48dp"
-            android:layout_gravity="center_horizontal"
-            android:gravity="center_horizontal"
-            android:orientation="vertical"/>
-
-        <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
-             of elevation and shadow) which is replaced by ripple effect in android:foreground -->
-        <RelativeLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="46dp"
-            android:layout_marginBottom="48dp"
-            android:layout_gravity="center_horizontal">
-
-            <Button
-                android:id="@+id/back_gesture_tutorial_fragment_action_button"
-                android:layout_width="142dp"
-                android:layout_height="49dp"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_button_margin_start_end"
-                android:layout_alignParentEnd="true"
-                android:stateListAnimator="@null"
-                android:background="@drawable/back_gesture_tutorial_action_button_background"
-                android:foreground="?android:attr/selectableItemBackgroundBorderless"
-                style="@style/TextAppearance.BackGestureTutorial.ButtonLabel"/>
-
-            <Button
-                android:id="@+id/back_gesture_tutorial_fragment_action_text_button"
-                android:layout_width="142dp"
-                android:layout_height="49dp"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_button_margin_start_end"
-                android:layout_alignParentStart="true"
-                android:stateListAnimator="@null"
-                android:background="@null"
-                android:foreground="?android:attr/selectableItemBackgroundBorderless"
-                style="@style/TextAppearance.BackGestureTutorial.TextButtonLabel"/>
-
-        </RelativeLayout>
-
-    </LinearLayout>
-
-</RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml
similarity index 92%
rename from quickstep/res/layout/back_gesture_tutorial_activity.xml
rename to quickstep/res/layout/gesture_tutorial_activity.xml
index e894e89..4dc8913 100644
--- a/quickstep/res/layout/back_gesture_tutorial_activity.xml
+++ b/quickstep/res/layout/gesture_tutorial_activity.xml
@@ -14,6 +14,6 @@
     limitations under the License.
 -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/back_gesture_tutorial_fragment_container"
+    android:id="@+id/gesture_tutorial_fragment_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/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.xml b/quickstep/res/layout/overview_actions.xml
deleted file mode 100644
index ad5efb6..0000000
--- a/quickstep/res/layout/overview_actions.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-    <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" >
-        </Space>
-        <Button
-            android:id="@+id/action_screenshot"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableTop="@drawable/ic_screenshot"
-            android:text="@string/action_screenshot" />
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1" >
-        </Space>
-
-        <Button
-            android:id="@+id/action_share"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableTop="@drawable/ic_share"
-            android:text="@string/action_share" />
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1" >
-        </Space>
-    </LinearLayout>
-
-</merge>
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index 328c20b..e05688e 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -14,10 +14,55 @@
      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="wrap_content"
-    android:layout_height="wrap_content"
-    android:visibility="gone">
+<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/values/colors.xml b/quickstep/res/values/colors.xml
index 3583676..40da136 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -14,5 +14,6 @@
      limitations under the License.
 -->
 <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 a688f9a..0f2955b 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -13,11 +13,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources xmlns:tools="http://schemas.android.com/tools">
+<resources>
     <string name="task_overlay_factory_class" translatable="false"/>
 
     <!-- Activities which block home gesture -->
-    <string-array name="gesture_blocking_activities" tools:ignore="InconsistentArrays">
+    <string-array name="gesture_blocking_activities" translatable="false">
         <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
     </string-array>
 
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index dcc85d5..6737c5f 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -23,7 +23,10 @@
     <dimen name="task_corner_radius_small">2dp</dimen>
 
     <!-- Overrideable in overlay that provides the Overview Actions. -->
-    <dimen name="overview_actions_height">110dp</dimen>
+    <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>
@@ -59,7 +62,8 @@
     <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">24dp</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:
@@ -72,14 +76,30 @@
     <dimen name="gestures_assistant_drag_threshold">55dp</dimen>
     <dimen name="gestures_assistant_fling_threshold">55dp</dimen>
 
+    <!-- One-Handed Mode -->
+    <!-- Threshold for draging distance to enable one-handed mode -->
+    <dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
+
     <!-- 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="back_gesture_tutorial_title_margin_start_end">40dp</dimen>
-    <dimen name="back_gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
-    <dimen name="back_gesture_tutorial_button_margin_start_end">18dp</dimen>
+    <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
+    <dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+    <dimen name="gesture_tutorial_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 f5552f0..86120e3 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -61,18 +61,14 @@
     <string name="all_apps_prediction_tip">Your predicted apps</string>
 
     <!-- Content description for a close button. [CHAR LIMIT=NONE] -->
-    <string  name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
-
-
-    <!-- Hotseat migration notification title -->
-    <string name="hotseat_edu_prompt_title">Easily access your most-used apps</string>
-    <!-- Hotseat migration notification content -->
-    <string name="hotseat_edu_prompt_content">Pixel predicts apps you\’ll need next, right on your Home screen. Tap to set up.</string>
+    <string  name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
     <!-- 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 -->
@@ -80,8 +76,8 @@
     <!-- Button text to dismiss opt in for fully predicted hotseat -->
     <string name="hotseat_edu_dismiss">No thanks</string>
 
-    <!-- action shown to turn off predictions after onboarding -->
-    <string name="hotseat_turn_off">Settings</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>
@@ -89,6 +85,10 @@
     <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>
@@ -97,25 +97,70 @@
     <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>
 
-    <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
-    <string name="back_gesture_tutorial_confirm_title" translatable="false">All set</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>
 
-    <!-- Button text shown on a button on the confirm screen. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_action_button_label" translatable="false">Done</string>
-    <!-- Button text shown on a text button on the confirm screen. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
+    <!-- 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 bf107fb..8d054b4 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -26,47 +26,63 @@
         <item name="android:layout_height">wrap_content</item>
     </style>
 
-    <style name="TextAppearance.BackGestureTutorial"
+    <style name="TextAppearance.GestureTutorial"
         parent="android:TextAppearance.Material.Body1" />
 
-    <style name="TextAppearance.BackGestureTutorial.CallToAction"
+    <style name="TextAppearance.GestureTutorial.CallToAction"
         parent="android:TextAppearance.Material.Body2" />
 
-    <style name="TextAppearance.BackGestureTutorial.Title"
-        parent="TextAppearance.BackGestureTutorial">
+    <style name="TextAppearance.GestureTutorial.Title"
+        parent="TextAppearance.GestureTutorial">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_title_color</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">28sp</item>
     </style>
 
-    <style name="TextAppearance.BackGestureTutorial.Subtitle"
-        parent="TextAppearance.BackGestureTutorial">
+    <style name="TextAppearance.GestureTutorial.Subtitle"
+        parent="TextAppearance.GestureTutorial">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_subtitle_color</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.BackGestureTutorial.ButtonLabel"
-        parent="TextAppearance.BackGestureTutorial.CallToAction">
+    <style name="TextAppearance.GestureTutorial.Feedback"
+        parent="TextAppearance.GestureTutorial">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_action_button_label_color</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.BackGestureTutorial.TextButtonLabel"
-        parent="TextAppearance.BackGestureTutorial.ButtonLabel">
-        <item name="android:textColor">@color/back_gesture_tutorial_primary_color</item>
+    <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">?attr/workspaceTextColor</item>
-        <item name="android:drawableTint">?attr/workspaceTextColor</item>
+        <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">4dp</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/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 53f37c1..22d205a 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -59,6 +59,7 @@
         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);
@@ -67,53 +68,114 @@
     }
 
     @Test
-    public void disabledMultipeRegions_shouldOverrideFirstRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
-        DefaultDisplay.Info info2 = createDisplayInfo(Surface.ROTATION_90);
-        mTouchTransformer.createOrAddTouchRegion(info2);
+    public void disabledMultipleRegions_shouldOverrideFirstRegion() {
+        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
 
-        float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        MotionEvent inOldRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inOldRegion);
-        assertFalse(mTouchTransformer.touchInValidSwipeRegions(inOldRegion.getX(), inOldRegion.getY()));
+        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);
-        inOldRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inOldRegion);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inOldRegion.getX(), inOldRegion.getY()));
+        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 allowMultipeRegions_shouldOverrideFirstRegion() {
-        DefaultDisplay.Info info2 = createDisplayInfo(Surface.ROTATION_90);
-        mTouchTransformer.createOrAddTouchRegion(info2);
+    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);
 
-        float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        MotionEvent inNewRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inNewRegion);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inNewRegion.getX(), inNewRegion.getY()));
+        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);
-        MotionEvent outOfRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, 100);
-        mTouchTransformer.transform(outOfRegion);
-        assertFalse(mTouchTransformer.touchInValidSwipeRegions(outOfRegion.getX(), outOfRegion.getY()));
+        tapAndAssertFalse(100, 100,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
     public void applyTransform_taskFrozen_noRotate_outOfRegion() {
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        MotionEvent outOfRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, 100);
-        mTouchTransformer.transform(outOfRegion);
-        assertFalse(mTouchTransformer.touchInValidSwipeRegions(outOfRegion.getX(), outOfRegion.getY()));
+        tapAndAssertFalse(100, 100,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
@@ -121,27 +183,24 @@
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        MotionEvent inRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inRegion);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion.getX(), inRegion.getY()));
+        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;
-        MotionEvent inRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inRegion);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion.getX(), inRegion.getY()));
+        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;
-        MotionEvent inRegion = generateMotionEvent(MotionEvent.ACTION_DOWN, 100, y);
-        mTouchTransformer.transform(inRegion);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion.getX(), inRegion.getY()));
+        tapAndAssertTrue(100, y,
+                event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
@@ -160,15 +219,15 @@
         MotionEvent inRegion2 = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, 10);
         mTouchTransformer.transform(inRegion1_down);
         mTouchTransformer.transform(inRegion2);
-        assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion1_down.getX(), inRegion1_down.getY()));
+        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 = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, 370);
-        mTouchTransformer.transform(inRegion2);
+        inRegion2 = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, 10, 370);
         assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
     }
 
@@ -191,4 +250,26 @@
     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
index abdff0d..47ce320 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -17,14 +17,8 @@
 
 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.FLAG_HIDE_BACK_BUTTON;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 
@@ -32,31 +26,32 @@
 import android.animation.ValueAnimator;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.view.View;
 
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.accessibility.SystemActions;
 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.touch.PagedOrientationHandler;
+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;
@@ -70,7 +65,6 @@
         implements NavigationModeChangeListener {
 
     private DepthController mDepthController = new DepthController(this);
-    protected SystemActions mSystemActions;
 
     /**
      * Reusable command for applying the back button alpha on the background thread.
@@ -81,53 +75,15 @@
 
     private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
 
-    private View mActionsView;
+    private OverviewActionsView mActionsView;
+    protected HotseatPredictionController mHotseatPredictionController;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mSystemActions = new SystemActions(this);
 
         SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
-
-        if (!getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) { }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE
-                            .get(BaseQuickstepLauncher.this).getMode().hasGestures;
-                    LauncherState prevState = getStateManager().getLastState();
-
-                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
-                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT
-                            <= getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
-                        getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-                        getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-
-        if (!getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
-            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) { }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    LauncherState prevState = getStateManager().getLastState();
-
-                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT
-                            <= getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
-                        getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
-                        getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
+        addMultiWindowModeChangedListener(mDepthController);
     }
 
     @Override
@@ -139,12 +95,9 @@
     @Override
     public void onNavigationModeChanged(Mode newMode) {
         getDragLayer().recreateControllers();
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-        mSystemActions.onActivityResult(requestCode);
+        if (mActionsView != null && isOverviewActionsEnabled()) {
+            mActionsView.updateVerticalMargin(newMode);
+        }
     }
 
     @Override
@@ -203,6 +156,7 @@
 
     @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);
@@ -210,34 +164,30 @@
             // removes the task itself.
             startActivity(ProxyActivityStarter.getLaunchIntent(this, null));
         }
-
-        // Register all system actions once they are available
-        mSystemActions.register();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        mSystemActions.unregister();
     }
 
     @Override
     protected void setupViews() {
         super.setupViews();
+
+        SysUINavigationMode.INSTANCE.get(this).updateMode();
         mActionsView = findViewById(R.id.overview_actions_view);
+        ((RecentsView) getOverviewPanel()).init(mActionsView);
 
-
-        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this)) {
+        if (isOverviewActionsEnabled()) {
             // Overview is above all other launcher elements, including qsb, so move it to the top.
             getOverviewPanel().bringToFront();
-            if (mActionsView != null) {
-                mActionsView.bringToFront();
-            }
+            mActionsView.bringToFront();
+            mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
         }
     }
 
-    public View getActionsView() {
-        return mActionsView;
+    private boolean isOverviewActionsEnabled() {
+        return FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this);
+    }
+
+    public <T extends OverviewActionsView> T getActionsView() {
+        return (T) mActionsView;
     }
 
     @Override
@@ -248,7 +198,7 @@
     }
 
     @Override
-    protected StateHandler[] createStateHandlers() {
+    protected StateHandler<LauncherState>[] createStateHandlers() {
         return new StateHandler[] {
                 getAllAppsController(),
                 getWorkspace(),
@@ -262,14 +212,8 @@
     }
 
     @Override
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
-            PagedOrientationHandler layoutVertical =
-                ((RecentsView)getOverviewPanel()).getPagedViewOrientedState().getOrientationHandler();
-            return layoutVertical.getScaleAndTranslation(getDeviceProfile(),
-                getOverviewPanel());
-        }
-        return super.getOverviewScaleAndTranslationForNormalState();
+    protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
+        return new QuickstepOnboardingPrefs(this, sharedPrefs);
     }
 
     @Override
@@ -295,6 +239,12 @@
     }
 
     @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();
     }
@@ -313,13 +263,10 @@
         super.onActivityFlagsChanged(changeBits);
     }
 
-    /**
-     * Sets the back button visibility based on the current state/window focus.
-     */
-    private void onLauncherStateOrFocusChanged() {
+    public boolean shouldBackButtonBeHidden(LauncherState toState) {
         Mode mode = SysUINavigationMode.getMode(this);
         boolean shouldBackButtonBeHidden = mode.hasGestures
-                && getStateManager().getState().hideBackButton
+                && toState.hasFlag(FLAG_HIDE_BACK_BUTTON)
                 && hasWindowFocus()
                 && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0;
         if (shouldBackButtonBeHidden) {
@@ -327,6 +274,14 @@
             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) {
@@ -352,4 +307,15 @@
     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 4ff0a72..02769c6 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -37,6 +37,8 @@
 public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat,
         WrappedAnimationRunnerImpl {
 
+    private static final String TAG = "LauncherAnimationRunner";
+
     private final Handler mHandler;
     private final boolean mStartAtFrontOfQueue;
     private AnimationResult mAnimationResult;
@@ -151,7 +153,8 @@
 
                 // 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()));
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index fbd7a8a..7fb0d43 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -23,7 +23,6 @@
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import com.android.launcher3.util.ActivityTracker;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -45,7 +44,7 @@
     }
 
     @Override
-    public boolean init(Launcher launcher, boolean alreadyOnHome) {
+    public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
         if (mRemoteAnimationProvider != null) {
             QuickstepAppTransitionManagerImpl appTransitionManager =
                     (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
@@ -71,7 +70,7 @@
             }, cancellationSignal);
         }
         launcher.deferOverlayCallbacksUntilNextResumeOrStop();
-        return super.init(launcher, alreadyOnHome);
+        return super.handleInit(launcher, alreadyOnHome);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index a30e102..10f789d 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -32,6 +32,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;
@@ -81,6 +82,7 @@
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 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;
@@ -88,7 +90,6 @@
 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;
 
@@ -291,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());
+                }
+            });
         }
     }
 
@@ -445,9 +455,9 @@
 
         RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
                 wallpaperTargets, MODE_OPENING);
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(floatingView);
-        openingTargets.addDependentTransactionApplier(surfaceApplier);
+        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.
@@ -484,7 +494,7 @@
                 : 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();
 
@@ -522,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);
@@ -533,40 +543,48 @@
 
             @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;
@@ -575,34 +593,30 @@
                     RemoteAnimationTargetCompat target = appTargets[i];
                     SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
 
-                    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);
-                    }
-
                     if (target.mode == MODE_OPENING) {
                         matrix.setScale(scale, scale);
-                        matrix.postTranslate(transX0, transY0);
-                        matrix.mapRect(currentBounds, targetBounds);
-                        if (mDeviceProfile.isVerticalBarLayout()) {
-                            currentBounds.right -= croppedWidth;
-                        } else {
-                            currentBounds.bottom -= croppedHeight;
-                        }
-                        floatingView.update(currentBounds, mIconAlpha.value, percent, 0f,
+                        matrix.postTranslate(windowTransX0, windowTransY0);
+
+                        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(target.screenSpaceBounds)
+                                .withWindowCrop(crop)
                                 .withAlpha(1f);
                     }
-                    builder.withLayer(RemoteAnimationProvider.getLayer(target, MODE_OPENING));
                     params[i] = builder.build();
                 }
                 surfaceApplier.scheduleApply(params);
@@ -636,6 +650,9 @@
      */
     @Override
     public void registerRemoteAnimations() {
+        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+            return;
+        }
         if (hasControlRemoteAppTransitionPermission()) {
             mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
 
@@ -666,6 +683,9 @@
      */
     @Override
     public void unregisterRemoteAnimations() {
+        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+            return;
+        }
         if (hasControlRemoteAppTransitionPermission()) {
             new ActivityCompat(mLauncher).unregisterRemoteAnimations();
 
@@ -694,8 +714,7 @@
      */
     private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(mDragLayer);
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
         ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
         unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
         float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 :
@@ -709,7 +728,6 @@
                     params[i] = new SurfaceParams.Builder(target.leash)
                             .withAlpha(1f)
                             .withWindowCrop(target.screenSpaceBounds)
-                            .withLayer(RemoteAnimationProvider.getLayer(target, MODE_OPENING))
                             .withCornerRadius(cornerRadius)
                             .build();
                 }
@@ -724,8 +742,7 @@
      */
     private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(mDragLayer);
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
         Matrix matrix = new Matrix();
         Point tmpPos = new Point();
         ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
@@ -764,9 +781,10 @@
                         builder.withMatrix(matrix)
                                 .withAlpha(1f);
                     }
+                    final Rect crop = new Rect(target.screenSpaceBounds);
+                    crop.offsetTo(0, 0);
                     params[i] = builder
-                            .withWindowCrop(target.screenSpaceBounds)
-                            .withLayer(RemoteAnimationProvider.getLayer(target, MODE_CLOSING))
+                            .withWindowCrop(crop)
                             .build();
                 }
                 surfaceApplier.scheduleApply(params);
diff --git a/quickstep/src/com/android/launcher3/accessibility/SystemActions.java b/quickstep/src/com/android/launcher3/accessibility/SystemActions.java
deleted file mode 100644
index 669877f..0000000
--- a/quickstep/src/com/android/launcher3/accessibility/SystemActions.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.accessibility;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Icon;
-import android.view.accessibility.AccessibilityManager;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.R;
-
-/**
- * Manages the launcher system actions presented to accessibility services.
- */
-public class SystemActions {
-
-    /**
-     * System Action ID to show all apps.  This ID should follow the ones in
-     * com.android.systemui.accessibility.SystemActions.
-     */
-    private static final int SYSTEM_ACTION_ID_ALL_APPS = 100;
-
-    private Launcher mLauncher;
-    private AccessibilityManager mAccessibilityManager;
-    private RemoteAction mAllAppsAction;
-    private boolean mRegistered;
-
-    public SystemActions(Launcher launcher) {
-        mLauncher = launcher;
-        mAccessibilityManager = (AccessibilityManager) launcher.getSystemService(
-                Context.ACCESSIBILITY_SERVICE);
-        mAllAppsAction = new RemoteAction(
-                Icon.createWithResource(launcher, R.drawable.ic_apps),
-                launcher.getString(R.string.all_apps_label),
-                launcher.getString(R.string.all_apps_label),
-                launcher.createPendingResult(SYSTEM_ACTION_ID_ALL_APPS, new Intent(),
-                        0 /* flags */));
-    }
-
-    public void register() {
-        if (mRegistered) {
-            return;
-        }
-        mAccessibilityManager.registerSystemAction(mAllAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
-        mRegistered = true;
-    }
-
-    public void unregister() {
-        if (!mRegistered) {
-            return;
-        }
-        mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
-        mRegistered = false;
-    }
-
-    public void onActivityResult(int requestCode) {
-        if (requestCode == SYSTEM_ACTION_ID_ALL_APPS) {
-            showAllApps();
-        }
-    }
-
-    private void showAllApps() {
-        LauncherStateManager stateManager = mLauncher.getStateManager();
-        stateManager.goToState(NORMAL);
-        stateManager.goToState(ALL_APPS);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 92c8573..f42b124 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -45,8 +45,8 @@
 import androidx.annotation.MainThread;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
 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;
@@ -77,6 +77,7 @@
     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);
@@ -221,6 +222,7 @@
             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
@@ -281,9 +283,9 @@
                 // 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);
+                        .getActivityList(null, Process.myUserHandle()).stream()
+                        .map(li -> li.getApplicationInfo().packageName).distinct()
+                        .toArray(String[]::new);
                 if (!updateActions(packageNames)) {
                     scheduleRefreshRetry(msg);
                 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
index 983702a..13501a4 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
@@ -21,8 +21,8 @@
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
 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;
@@ -32,7 +32,7 @@
 /**
  * State handler for animating back button alpha
  */
-public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler {
+public class BackButtonAlphaHandler implements StateHandler<LauncherState> {
 
     private final BaseQuickstepLauncher mLauncher;
     private final AnimatedFloat mBackAlpha = new AnimatedFloat(this::updateBackAlpha);
@@ -59,7 +59,8 @@
         }
 
         mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastBackButtonAlpha();
-        animation.setFloat(mBackAlpha, VALUE, toState.hideBackButton ? 0 : 1, LINEAR);
+        animation.setFloat(mBackAlpha, VALUE,
+                mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR);
     }
 
     private void updateBackAlpha() {
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 24ba89a..fe8f0c6 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -17,19 +17,26 @@
 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.LauncherStateManager;
 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;
@@ -38,7 +45,8 @@
 /**
  * Controls blur and wallpaper zoom, for the Launcher surface only.
  */
-public class DepthController implements LauncherStateManager.StateHandler {
+public class DepthController implements StateHandler<LauncherState>,
+        BaseActivity.MultiWindowModeChangedListener {
 
     public static final FloatProperty<DepthController> DEPTH =
             new FloatProperty<DepthController>("depth") {
@@ -101,16 +109,37 @@
      */
     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) {
-            return;
+        if (mWallpaperManager == null) {
+            mMaxBlurRadius = mLauncher.getResources().getInteger(R.integer.max_depth_blur_radius);
+            mWallpaperManager = new WallpaperManagerCompat(mLauncher);
         }
-        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);
+        }
     }
 
     /**
@@ -150,7 +179,7 @@
 
     @Override
     public void setState(LauncherState toState) {
-        if (mSurface == null) {
+        if (mSurface == null || mIgnoreStateChangesDuringMultiWindowAnimation) {
             return;
         }
 
@@ -163,28 +192,69 @@
     @Override
     public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
             PendingAnimation animation) {
-        if (mSurface == null || config.onlyPlayAtomicComponent()) {
+        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, LINEAR);
+            animation.setFloat(this, DEPTH, toDepth, config.getInterpolator(ANIM_DEPTH, LINEAR));
         }
     }
 
     private void setDepth(float depth) {
-        mDepth = depth;
-        if (mSurface == null || !mSurface.isValid()) {
+        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);
         }
-        new TransactionCompat()
-                .setBackgroundBlurRadius(mSurface, (int) (mDepth * mMaxBlurRadius))
-                .apply();
+
+        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/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 123c988..ec3a490 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -17,32 +17,30 @@
 package com.android.launcher3.uioverrides;
 
 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.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.ANIM_OVERVIEW_TRANSLATE_Y;
 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 androidx.annotation.NonNull;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.StateHandler;
 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
@@ -50,8 +48,8 @@
  *
  * @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 BaseQuickstepLauncher mLauncher;
 
@@ -62,15 +60,14 @@
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        ScaleAndTranslation scaleAndTranslation = state
-                .getOverviewScaleAndTranslation(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleAndTranslation.scale);
-        mRecentsView.setTranslationX(scaleAndTranslation.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
@@ -95,21 +92,26 @@
      */
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
-        ScaleAndTranslation scaleAndTranslation = toState.getOverviewScaleAndTranslation(mLauncher);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndTranslation.scale,
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_X, scaleAndTranslation.translationX,
+        setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_Y, scaleAndTranslation.translationY,
-                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
         setter.setFloat(scrim, SCRIM_PROGRESS, toState.getOverviewScrimAlpha(mLauncher),
                 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/PreviewSurfaceRenderer.java b/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
deleted file mode 100644
index c7cce0b..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Size;
-import android.view.View;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.graphics.LauncherPreviewRenderer;
-import com.android.systemui.shared.system.SurfaceViewRequestReceiver;
-
-/** Render preview using surface view. */
-public class PreviewSurfaceRenderer {
-
-    /** Handle a received surface view request. */
-    public static void render(Context context, Bundle bundle) {
-        String gridName = bundle.getString("name");
-        bundle.remove("name");
-        if (gridName == null) {
-            gridName = InvariantDeviceProfile.getCurrentGridName(context);
-        }
-        final InvariantDeviceProfile idp = new InvariantDeviceProfile(context, gridName);
-
-        MAIN_EXECUTOR.execute(() -> {
-            View view = new LauncherPreviewRenderer(context, idp).getRenderedView();
-            new SurfaceViewRequestReceiver().onReceive(context, bundle, view,
-                    new Size(view.getMeasuredWidth(), view.getMeasuredHeight()));
-        });
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 93e02a1..e7cd393 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -16,10 +16,11 @@
 package com.android.launcher3.uioverrides.states;
 
 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 android.content.Context;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -31,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
@@ -45,23 +46,11 @@
     }
 
     @Override
-    public int getTransitionDuration(Launcher launcher) {
+    public int getTransitionDuration(Context context) {
         return 320;
     }
 
     @Override
-    public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViews(launcher);
-        dispatchWindowStateChanged(launcher);
-    }
-
-    @Override
-    public void onStateDisabled(Launcher launcher) {
-        super.onStateDisabled(launcher);
-        AbstractFloatingView.closeAllOpenViews(launcher);
-    }
-
-    @Override
     public String getDescription(Launcher launcher) {
         AllAppsContainerView appsView = launcher.getAppsView();
         return appsView.getDescription();
@@ -76,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;
@@ -87,7 +76,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
@@ -102,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/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index cc3fd97..20ee61d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -16,24 +16,28 @@
 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.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.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;
@@ -41,6 +45,7 @@
 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;
@@ -49,6 +54,7 @@
 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.
@@ -119,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;
@@ -127,13 +133,33 @@
 
     @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) {
+            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
@@ -237,8 +263,9 @@
             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, config)
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index c3b90e3..f7e8781 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -38,11 +38,17 @@
                 }
             };
 
+    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
index 94ef15a..2b698bd 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,27 +15,46 @@
  */
 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.graphics.RectF;
 import android.os.Build;
-import android.util.Pair;
 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.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;
 
@@ -46,97 +65,243 @@
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public interface BaseActivityInterface<T extends BaseDraggingActivity> {
+public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
+        ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
 
-    void onTransitionCancelled(boolean activityVisible);
+    public final boolean rotationSupportedByActivity;
 
-    int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect);
+    private final STATE_TYPE mOverviewState, mBackgroundState;
 
-    /**
-     * @return The progress of the swipe where we start resisting the user, where 0 is fullscreen
-     * and 1 is recents. These values should probably be greater than 1 to let the user swipe past
-     * recents before we start resisting them.
-     */
-    default Pair<Float, Float> getSwipeUpPullbackStartAndMaxProgress() {
-        return new Pair<>(1.4f, 1.8f);
+    protected BaseActivityInterface(boolean rotationSupportedByActivity,
+            STATE_TYPE overviewState, STATE_TYPE backgroundState) {
+        this.rotationSupportedByActivity = rotationSupportedByActivity;
+        mOverviewState = overviewState;
+        mBackgroundState = backgroundState;
     }
 
-    void onSwipeUpToRecentsComplete();
+    public void onTransitionCancelled(boolean activityVisible) {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        STATE_TYPE startState = activity.getStateManager().getRestState();
+        activity.getStateManager().goToState(startState, activityVisible);
+    }
 
-    default void onSwipeUpToHomeComplete() { }
-    void onAssistantVisibilityChanged(float visibility);
+    public abstract int getSwipeUpDestinationAndLength(
+            DeviceProfile dp, Context context, Rect outRect,
+            PagedOrientationHandler orientationHandler);
 
-    @NonNull HomeAnimationFactory prepareHomeUI();
+    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();
+    }
 
-    AnimationFactory prepareRecentsUI(boolean activityVisible, boolean animateActivity,
-            Consumer<AnimatorPlaybackController> callback);
+    public abstract void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState);
 
-    ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener);
+    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.
      */
-    default void setOnDeferredActivityLaunchCallback(Runnable r) {}
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {}
 
     @Nullable
-    T getCreatedActivity();
+    public abstract ACTIVITY_TYPE getCreatedActivity();
 
     @Nullable
-    default DepthController getDepthController() {
+    public DepthController getDepthController() {
         return null;
     }
 
-    default boolean isResumed() {
-        BaseDraggingActivity activity = getCreatedActivity();
+    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
-    <T extends View> T getVisibleRecentsView();
+    public abstract <T extends RecentsView> T getVisibleRecentsView();
 
     @UiThread
-    boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
 
-    Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target);
+    public abstract Rect getOverviewWindowBounds(
+            Rect homeBounds, RemoteAnimationTargetCompat target);
 
-    boolean shouldMinimizeSplitScreen();
+    public abstract boolean allowMinimizeSplitScreen();
 
-    default boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
-        return true;
+    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.
      */
-    default void updateOverviewPredictionState() {
-        // By default overview predictions are not supported
+    public void updateOverviewPredictionState() {
+        // By public overview predictions are not supported
     }
 
     /**
      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
      */
-    int getContainerType();
+    public abstract int getContainerType();
 
-    boolean isInLiveTileMode();
+    public abstract boolean isInLiveTileMode();
 
-    void onLaunchTaskFailed();
+    public abstract void onLaunchTaskFailed();
 
-    void onLaunchTaskSuccess();
+    public void onLaunchTaskSuccess() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.getStateManager().moveToRestState();
+    }
 
-    default void closeOverlay() { }
+    public void closeOverlay() { }
 
-    default void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable runnable) {}
+    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);
+    }
 
-    interface AnimationFactory {
+    /**
+     * 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);
+    }
 
-        default void onRemoteAnimationReceived(RemoteAnimationTargets targets) { }
+    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 adjustActivityControllerInterpolators() { }
-
         default void onTransitionCancelled() { }
 
         default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
@@ -150,34 +315,98 @@
         default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
     }
 
-    interface HomeAnimationFactory {
+    class DefaultAnimationFactory implements AnimationFactory {
 
-        /** Return the floating view that will animate in sync with the closing window. */
-        default @Nullable View getFloatingView() {
-            return null;
+        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();
         }
 
-        @NonNull RectF getWindowTargetRect();
-
-        @NonNull AnimatorPlaybackController createActivityAnimationToHome();
-
-        default void playAtomicAnimation(float velocity) {
-            // No-op
+        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;
         }
 
-        static RectF getDefaultWindowTargetRect(PagedOrientationHandler orientationHandler,
-            DeviceProfile dp) {
-            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);
+        @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 1b9158b..0000000
--- a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
+++ /dev/null
@@ -1,168 +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.util.ActivityTracker;
-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 {
-
-    public static final ActivityTracker<BaseRecentsActivity> ACTIVITY_TRACKER =
-            new ActivityTracker<>();
-    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));
-        ACTIVITY_TRACKER.handleCreate(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();
-        // 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 onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        ACTIVITY_TRACKER.handleNewIntent(this, intent);
-    }
-
-    @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
-    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
index 544f420..00b5eb9 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -17,21 +17,27 @@
 
 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.BaseDraggingActivity;
+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 {
 
     /**
@@ -44,7 +50,7 @@
 
         NEW_TASK(false, ContainerType.APP, true),
 
-        LAST_TASK(false, ContainerType.APP, false);
+        LAST_TASK(false, ContainerType.APP, true);
 
         GestureEndTarget(boolean isLauncher, int containerType,
                 boolean recentsAttachedToAppWindow) {
@@ -106,14 +112,14 @@
     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");
 
-    // Called when the new task appeared from quick switching.
-    public static final int STATE_TASK_APPEARED_DURING_SWITCH =
-            getFlagForIndex("STATE_TASK_APPEARED_DURING_SWITCH");
-
     // Needed to interact with the current activity
     private final Intent mHomeIntent;
     private final Intent mOverviewIntent;
@@ -123,9 +129,9 @@
 
     private ActivityManager.RunningTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
-    private RemoteAnimationTargetCompat mAnimationTarget;
-    // TODO: This can be removed once we stop finishing the animation when starting a new task
-    private int mFinishingRecentsAnimationTaskId = -1;
+    private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
+    private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
+    private int mLastStartedTaskId = -1;
 
     public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
         mHomeIntent = componentObserver.getHomeIntent();
@@ -143,7 +149,9 @@
         mGestureId = other.mGestureId;
         mRunningTask = other.mRunningTask;
         mEndTarget = other.mEndTarget;
-        mFinishingRecentsAnimationTaskId = other.mFinishingRecentsAnimationTaskId;
+        mLastAppearedTaskTarget = other.mLastAppearedTaskTarget;
+        mPreviouslyAppearedTaskIds = other.mPreviouslyAppearedTaskIds;
+        mLastStartedTaskId = other.mLastStartedTaskId;
     }
 
     public GestureState() {
@@ -193,7 +201,7 @@
     /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
-    public <T extends BaseDraggingActivity> BaseActivityInterface<T> getActivityInterface() {
+    public <T extends StatefulActivity<?>> BaseActivityInterface<?, T> getActivityInterface() {
         return mActivityInterface;
     }
 
@@ -226,20 +234,52 @@
     }
 
     /**
+     * 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;
     }
 
-    public void setAnimationTarget(RemoteAnimationTargetCompat target) {
-        mAnimationTarget = target;
-    }
-
-    public RemoteAnimationTargetCompat getAnimationTarget() {
-        return mAnimationTarget;
-    }
-
     /**
      * Sets the end target of this gesture and immediately notifies the state changes.
      */
@@ -254,35 +294,15 @@
     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 the id for the task that was about to be launched following the finish of the recents
-     * animation.  Only defined between when the finish-recents call was made and the launch
-     * activity call is made.
-     */
-    public int getFinishingRecentsAnimationTaskId() {
-        return mFinishingRecentsAnimationTaskId;
-    }
-
-    /**
-     * Sets the id for the task will be launched after the recents animation is finished. Once the
-     * animation has finished then the id will be reset to -1.
-     */
-    public void setFinishingRecentsAnimationTaskId(int taskId) {
-        mFinishingRecentsAnimationTaskId = taskId;
-        mStateCallback.runOnceAtState(STATE_RECENTS_ANIMATION_FINISHED, () -> {
-            mFinishingRecentsAnimationTaskId = -1;
-        });
-    }
-
-    /**
      * @return whether the current gesture is still running a recents animation to a state in the
      *         Launcher or Recents activity.
-     * Updates the running task for the gesture to be the given {@param runningTask}.
      */
     public boolean isRunningAnimationToLauncher() {
         return isRecentsAnimationRunning() && mEndTarget != null && mEndTarget.isLauncher;
@@ -314,18 +334,13 @@
         mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
     }
 
-    @Override
-    public void onTaskAppeared(RemoteAnimationTargetCompat app) {
-        mAnimationTarget = app;
-        mStateCallback.setState(STATE_TASK_APPEARED_DURING_SWITCH);
-    }
-
     public void dump(PrintWriter pw) {
         pw.println("GestureState:");
         pw.println("  gestureID=" + mGestureId);
         pw.println("  runningTask=" + mRunningTask);
         pw.println("  endTarget=" + mEndTarget);
-        pw.println("  finishingRecentsAnimationTaskId=" + mFinishingRecentsAnimationTaskId);
+        pw.println("  lastAppearedTaskTargetId=" + getLastAppearedTaskId());
+        pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
         pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
     }
 }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 818d836..67711c0 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -34,6 +34,8 @@
     int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
     int TYPE_RESET_GESTURE = 1 << 8;
     int TYPE_OVERSCROLL = 1 << 9;
+    int TYPE_SYSUI_OVERLAY = 1 << 10;
+    int TYPE_ONE_HANDED = 1 << 11;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -46,6 +48,8 @@
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
             "TYPE_OVERSCROLL",              // 9
+            "TYPE_SYSUI_OVERLAY",           // 10
+            "TYPE_ONE_HANDED",              // 11
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
@@ -70,10 +74,15 @@
     }
 
     /**
-     * Returns true if the given input consumer is in the hierarchy of this input consumer.
+     * Handle and specific setup necessary based on the orientation of the device
      */
-    default boolean isInConsumerHierarchy(InputConsumer candidate) {
-        return this == candidate;
+    default void notifyOrientationSetup() {}
+
+    /**
+     * Returns the active input consumer is in the hierarchy of this input consumer.
+     */
+    default InputConsumer getActiveConsumerInHierarchy() {
+        return this;
     }
 
     /**
@@ -96,15 +105,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/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 3e73f49..0710a0a 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -22,6 +22,9 @@
 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;
@@ -33,7 +36,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
-import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 
 import java.io.PrintWriter;
@@ -52,35 +55,83 @@
     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 mCurrentRotation;
+    private final RectF mOneHandedModeRegion = new RectF();
+    private int mCurrentDisplayRotation;
+    private int mNavBarGesturalHeight;
     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;
-    private int mQuickStepStartingRotation = -1;
+
+    /**
+     * 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;
+        mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
     }
 
-    void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info) {
+    private void refreshTouchRegion(DefaultDisplay.Info info, Resources newRes) {
+        // 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
+        mResources = newRes;
+        mSwipeTouchRegions.clear();
+        resetSwipeRegions(info);
+    }
+
+    void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info,
+            Resources newRes) {
         if (mMode == newMode) {
             return;
         }
         this.mMode = newMode;
-        resetSwipeRegions(info);
+        refreshTouchRegion(info, newRes);
+    }
+
+    void setGesturalHeight(int newGesturalHeight, DefaultDisplay.Info info, Resources newRes) {
+        if (mNavBarGesturalHeight == newGesturalHeight) {
+            return;
+        }
+        mNavBarGesturalHeight = newGesturalHeight;
+        refreshTouchRegion(info, newRes);
     }
 
     /**
@@ -92,20 +143,21 @@
      * @see #enableMultipleRegions(boolean, DefaultDisplay.Info)
      */
     void createOrAddTouchRegion(DefaultDisplay.Info info) {
-        mCurrentRotation = info.rotation;
-        if (mQuickStepStartingRotation > -1 && mCurrentRotation == mQuickStepStartingRotation) {
-            // Ignore nav bars in other rotations except for the one we started out in
+        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(mCurrentRotation);
+        OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplayRotation);
         if (region != null) {
             return;
         }
 
         if (mEnableMultipleRegions) {
-            mSwipeTouchRegions.put(mCurrentRotation, createRegionForDisplay(info));
+            mSwipeTouchRegions.put(mCurrentDisplayRotation, createRegionForDisplay(info));
         } else {
             resetSwipeRegions(info);
         }
@@ -116,20 +168,32 @@
      * 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
+     * @param info The current displayInfo which will be the start of the quickswitch gesture
      */
     void enableMultipleRegions(boolean enableMultipleRegions, DefaultDisplay.Info info) {
-        mEnableMultipleRegions = enableMultipleRegions;
-        if (!enableMultipleRegions) {
-            mQuickStepStartingRotation = -1;
-            resetSwipeRegions(info);
+        mEnableMultipleRegions = enableMultipleRegions &&
+                mMode != SysUINavigationMode.Mode.TWO_BUTTONS;
+        if (mEnableMultipleRegions) {
+            mQuickStepStartingRotation = info.rotation;
         } else {
-            if (mLastRectTouched != null) {
-                // mLastRectTouched can be null if gesture type is changed (ex. from settings)
-                // but nav bar hasn't been interacted with yet.
-                mQuickStepStartingRotation = mLastRectTouched.mRotation;
-            }
+            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);
     }
 
     /**
@@ -140,40 +204,41 @@
      */
     private void resetSwipeRegions(DefaultDisplay.Info region) {
         if (DEBUG) {
-            Log.d(TAG, "clearing all regions except rotation: " + mCurrentRotation);
+            Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplayRotation);
         }
 
-        mCurrentRotation = region.rotation;
+        mCurrentDisplayRotation = region.rotation;
+        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        if (regionToKeep == null) {
+            regionToKeep = createRegionForDisplay(region);
+        }
         mSwipeTouchRegions.clear();
-        mSwipeTouchRegions.put(mCurrentRotation, createRegionForDisplay(region));
+        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: " + mCurrentRotation);
+            Log.d(TAG, "creating rotation region for: " + mCurrentDisplayRotation);
         }
 
         Point size = display.realSize;
         int rotation = display.rotation;
+        int touchHeight = mNavBarGesturalHeight;
         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;
-
-            final int assistantWidth = mResources
-                    .getDimensionPixelSize(R.dimen.gestures_assistant_width);
-            final float assistantHeight = Math.max(touchHeight,
-                    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;
+            updateAssistantRegions(orientationRectF);
         } else {
             mAssistantLeftRegion.setEmpty();
             mAssistantRightRegion.setEmpty();
@@ -187,25 +252,49 @@
                             + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
                     break;
                 default:
-                    orientationRectF.top = orientationRectF.bottom
-                            - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+                    orientationRectF.top = orientationRectF.bottom - touchHeight;
             }
         }
+        // One handed gestural only active on portrait mode
+        mOneHandedModeRegion.set(0, orientationRectF.bottom - touchHeight, size.x, size.y);
 
         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());
 
     }
 
+    boolean touchInOneHandedModeRegion(MotionEvent ev) {
+        return mOneHandedModeRegion.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);
         }
@@ -213,11 +302,11 @@
     }
 
     int getCurrentActiveRotation() {
-        if (mLastRectTouched == null) {
-            return 0;
-        } else {
-            return mLastRectTouched.mRotation;
-        }
+        return mActiveTouchRotation;
+    }
+
+    int getQuickStepStartingRotation() {
+        return mQuickStepStartingRotation;
     }
 
     public void transform(MotionEvent event) {
@@ -247,11 +336,25 @@
 
                 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);
                         }
@@ -274,17 +377,12 @@
             regions.append(rectF.mRotation).append(" ");
         }
         pw.println(regions.toString());
+        pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
+        pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
     }
 
     private class OrientationRectF extends RectF {
 
-        /**
-         * Delta to subtract width and height by because if we report the translated touch
-         * bounds as the width and height, calling {@link RectF#contains(float, float)} will
-         * be false
-         */
-        private float maxDelta = 0.001f;
-
         private int mRotation;
         private float mHeight;
         private float mWidth;
@@ -292,8 +390,8 @@
         OrientationRectF(float left, float top, float right, float bottom, int rotation) {
             super(left, top, right, bottom);
             this.mRotation = rotation;
-            mHeight = bottom - maxDelta;
-            mWidth = right - maxDelta;
+            mHeight = bottom;
+            mWidth = right;
         }
 
         @Override
@@ -303,40 +401,39 @@
             return s;
         }
 
-        boolean applyTransform(MotionEvent event, boolean forceTransform) {
-            // TODO(b/149658423): See if we can use RotationHelper.getRotationMatrix here
-            MotionEvent tmp = MotionEvent.obtain(event);
-            Matrix outMatrix = new Matrix();
-            int delta = RotationHelper.deltaRotation(mCurrentRotation, mRotation);
-            switch (delta) {
-                case Surface.ROTATION_0:
-                    outMatrix.reset();
-                    break;
-                case Surface.ROTATION_90:
-                    outMatrix.setRotate(270);
-                    outMatrix.postTranslate(0, mHeight);
-                    break;
-                case Surface.ROTATION_180:
-                    outMatrix.setRotate(180);
-                    outMatrix.postTranslate(mHeight, mWidth);
-                    break;
-                case Surface.ROTATION_270:
-                    outMatrix.setRotate(90);
-                    outMatrix.postTranslate(mWidth, 0);
-                    break;
-            }
+        @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;
+        }
 
-            tmp.transform(outMatrix);
+        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: " + tmp.getX() + ", " + tmp.getY()
+                                + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
                                 + " rect: " + this + " forceTransform: " + forceTransform
-                                + " contains: " + contains(tmp.getX(), tmp.getY()));
+                                + " contains: " + contains(mTmpPoint[0], mTmpPoint[1]));
             }
 
-            if (forceTransform || contains(tmp.getX(), tmp.getY())) {
-                event.transform(outMatrix);
-                tmp.recycle();
+            if (contains(mTmpPoint[0], mTmpPoint[1])) {
+                event.transform(mTmpMatrix);
                 return true;
             }
             return false;
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 9edc86e..07f838b 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -20,6 +20,7 @@
 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;
@@ -40,6 +41,7 @@
 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
@@ -57,6 +59,9 @@
     private final Intent mMyHomeIntent;
     private final Intent mFallbackIntent;
     private final SparseIntArray mConfigChangesMap = new SparseIntArray();
+
+    private Consumer<Boolean> mOverviewChangeListener = b -> { };
+
     private String mUpdateRegisteredPackage;
     private BaseActivityInterface mActivityInterface;
     private Intent mOverviewIntent;
@@ -64,10 +69,10 @@
     private boolean mIsDefaultHome;
     private boolean mIsHomeDisabled;
 
+
     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);
@@ -95,6 +100,13 @@
         updateOverviewTargets();
     }
 
+    /**
+     * Sets a listener for changes in {@link #isHomeAndOverviewSame()}
+     */
+    public void setOverviewChangeListener(Consumer<Boolean> overviewChangeListener) {
+        mOverviewChangeListener = overviewChangeListener;
+    }
+
     public void onSystemUiStateChanged() {
         if (mDeviceState.isHomeDisabled() != mIsHomeDisabled) {
             updateOverviewTargets();
@@ -127,9 +139,16 @@
             mActivityInterface.onAssistantVisibilityChanged(0.f);
         }
 
+        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.
-            mActivityInterface = new LauncherActivityInterface();
+            mActivityInterface = LauncherActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = true;
             mOverviewIntent = mMyHomeIntent;
             mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent());
@@ -139,7 +158,7 @@
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
 
-            mActivityInterface = new FallbackActivityInterface();
+            mActivityInterface = FallbackActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = false;
             mOverviewIntent = mFallbackIntent;
             mCurrentHomeIntent.setComponent(defaultHome);
@@ -159,6 +178,7 @@
                         ACTION_PACKAGE_REMOVED));
             }
         }
+        mOverviewChangeListener.accept(mIsHomeAndOverviewSame);
     }
 
     /**
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/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 566a701..a21c714 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -19,13 +19,11 @@
 
 import android.graphics.Rect;
 import android.util.ArraySet;
-import android.util.Log;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.Preconditions;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -41,15 +39,15 @@
         com.android.systemui.shared.system.RecentsAnimationListener {
 
     private final Set<RecentsAnimationListener> mListeners = new ArraySet<>();
-    private final boolean mShouldMinimizeSplitScreen;
+    private final boolean mAllowMinimizeSplitScreen;
 
     // TODO(141886704): Remove these references when they are no longer needed
     private RecentsAnimationController mController;
 
     private boolean mCancelled;
 
-    public RecentsAnimationCallbacks(boolean shouldMinimizeSplitScreen) {
-        mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+    public RecentsAnimationCallbacks(boolean allowMinimizeSplitScreen) {
+        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
     }
 
     @UiThread
@@ -91,13 +89,10 @@
             RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets,
             Rect homeContentInsets, Rect minimizedHomeBounds) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_FROM_RECENTS, "onAnimationStart");
-        }
         RecentsAnimationTargets targets = new RecentsAnimationTargets(appTargets,
                 wallpaperTargets, homeContentInsets, minimizedHomeBounds);
         mController = new RecentsAnimationController(animationController,
-                mShouldMinimizeSplitScreen, this::onAnimationFinished);
+                mAllowMinimizeSplitScreen, this::onAnimationFinished);
 
         if (mCancelled) {
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
@@ -164,6 +159,6 @@
         /**
          * Callback made when a task started from the recents is ready for an app transition.
          */
-        default void onTaskAppeared(RemoteAnimationTargetCompat app) {}
+        default void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {}
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 268e564..4e9aa61 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -22,13 +22,13 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
-import android.annotation.NonNull;
 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;
@@ -49,21 +49,22 @@
 
     private final RecentsAnimationControllerCompat mController;
     private final Consumer<RecentsAnimationController> mOnFinishedListener;
-    private final boolean mShouldMinimizeSplitScreen;
+    private final boolean mAllowMinimizeSplitScreen;
 
     private InputConsumerController mInputConsumerController;
     private Supplier<InputConsumer> mInputProxySupplier;
     private InputConsumer mInputConsumer;
-    private boolean mWindowThresholdCrossed = false;
+    private boolean mUseLauncherSysBarFlags = false;
+    private boolean mSplitScreenMinimized = false;
     private boolean mTouchInProgress;
-    private boolean mFinishPending;
+    private boolean mDisableInputProxyPending;
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
-            boolean shouldMinimizeSplitScreen,
+            boolean allowMinimizeSplitScreen,
             Consumer<RecentsAnimationController> onFinishedListener) {
         mController = controller;
         mOnFinishedListener = onFinishedListener;
-        mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+        mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
     }
 
     /**
@@ -76,16 +77,31 @@
 
     /**
      * Indicates that the gesture has crossed the window boundary threshold and system UI can be
-     * update the represent the window behind
+     * update the system bar flags accordingly.
      */
-    public void setWindowThresholdCrossed(boolean windowThresholdCrossed) {
-        if (mWindowThresholdCrossed != windowThresholdCrossed) {
-            mWindowThresholdCrossed = windowThresholdCrossed;
+    public void setUseLauncherSystemBarFlags(boolean useLauncherSysBarFlags) {
+        if (mUseLauncherSysBarFlags != useLauncherSysBarFlags) {
+            mUseLauncherSysBarFlags = useLauncherSysBarFlags;
             UI_HELPER_EXECUTOR.execute(() -> {
-                mController.setAnimationTargetsBehindSystemBars(!windowThresholdCrossed);
+                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 && mShouldMinimizeSplitScreen) {
-                    p.setSplitScreenMinimized(windowThresholdCrossed);
+                if (p != null) {
+                    p.setSplitScreenMinimized(splitScreenMinimized);
                 }
             });
         }
@@ -120,12 +136,12 @@
 
     @UiThread
     public void finishAnimationToHome() {
-        finishAndClear(true /* toRecents */, null, false /* sendUserLeaveHint */);
+        finishAndDisableInputProxy(true /* toRecents */, null, false /* sendUserLeaveHint */);
     }
 
     @UiThread
     public void finishAnimationToApp() {
-        finishAndClear(false /* toRecents */, null, false /* sendUserLeaveHint */);
+        finishAndDisableInputProxy(false /* toRecents */, null, false /* sendUserLeaveHint */);
     }
 
     /** See {@link #finish(boolean, Runnable, boolean)} */
@@ -144,22 +160,16 @@
     @UiThread
     public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
         Preconditions.assertUIThread();
-        if (!toRecents) {
-            finishAndClear(false, onFinishComplete, sendUserLeaveHint);
+        if (toRecents && mTouchInProgress) {
+            // Finish the controller as requested, but don't disable input proxy yet.
+            mDisableInputProxyPending = true;
+            finishController(toRecents, onFinishComplete, sendUserLeaveHint);
         } else {
-            if (mTouchInProgress) {
-                mFinishPending = true;
-                // Execute the callback
-                if (onFinishComplete != null) {
-                    onFinishComplete.run();
-                }
-            } else {
-                finishAndClear(true, onFinishComplete, sendUserLeaveHint);
-            }
+            finishAndDisableInputProxy(toRecents, onFinishComplete, sendUserLeaveHint);
         }
     }
 
-    private void finishAndClear(boolean toRecents, Runnable onFinishComplete,
+    private void finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete,
             boolean sendUserLeaveHint) {
         disableInputProxy();
         finishController(toRecents, onFinishComplete, sendUserLeaveHint);
@@ -247,9 +257,9 @@
         } 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 (mDisableInputProxyPending) {
+                mDisableInputProxyPending = false;
+                disableInputProxy();
             }
         }
         if (mInputConsumer != null) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 491c611..6b0c395 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -16,16 +16,22 @@
 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_ONE_HANDED_ACTIVE;
 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;
@@ -34,27 +40,32 @@
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 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.SystemProperties;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.text.TextUtils;
-import android.util.Log;
+import android.util.DisplayMetrics;
 import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
 
 import androidx.annotation.BinderThread;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 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.SysUINavigationMode.OneHandedModeChangeListener;
 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;
@@ -71,7 +82,8 @@
  */
 public class RecentsAnimationDeviceState implements
         NavigationModeChangeListener,
-        DefaultDisplay.DisplayInfoChangeListener {
+        DefaultDisplay.DisplayInfoChangeListener,
+        OneHandedModeChangeListener {
 
     private final Context mContext;
     private final SysUINavigationMode mSysUiNavMode;
@@ -88,6 +100,8 @@
     private final Region mDeferredGestureRegion = new Region();
     private boolean mAssistantAvailable;
     private float mAssistantVisibility;
+    private boolean mIsOneHandedModeEnabled;
+    private boolean mIsSwipeToNotificationEnabled;
 
     private boolean mIsUserUnlocked;
     private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
@@ -104,23 +118,77 @@
     private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
         @Override
         public void onRecentTaskListFrozenChanged(boolean frozen) {
-            if (frozen) {
+            mTaskListFrozen = frozen;
+            if (frozen || mInOverview) {
                 return;
             }
-            mOrientationTouchTransformer.enableMultipleRegions(false, mDefaultDisplay.getInfo());
+            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) {
-        final ContentResolver resolver = context.getContentResolver();
         mContext = context;
         mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
@@ -170,13 +238,56 @@
                         ComponentName.unflattenFromString(blockingActivity));
             }
         }
+
+        if (SystemProperties.getBoolean("ro.support_one_handed_mode", false)) {
+            SecureSettingsObserver oneHandedEnabledObserver =
+                    SecureSettingsObserver.newOneHandedSettingsObserver(
+                            mContext, enabled -> mIsOneHandedModeEnabled = enabled);
+            oneHandedEnabledObserver.register();
+            oneHandedEnabledObserver.dispatchOnChange();
+            runOnDestroy(oneHandedEnabledObserver::unregister);
+        }
+
+        SecureSettingsObserver swipeBottomEnabledObserver =
+                SecureSettingsObserver.newSwipeToNotificationSettingsObserver(
+                        mContext, enabled -> mIsSwipeToNotificationEnabled = enabled);
+        swipeBottomEnabledObserver.register();
+        swipeBottomEnabledObserver.dispatchOnChange();
+        runOnDestroy(swipeBottomEnabledObserver::unregister);
+
+        SecureSettingsObserver userSetupObserver = new SecureSettingsObserver(
+                context.getContentResolver(),
+                e -> mIsUserSetupComplete = e,
+                Settings.Secure.USER_SETUP_COMPLETE,
+                0);
+        mIsUserSetupComplete = userSetupObserver.getValue();
+        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() {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
-            return;
-        }
-
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
         mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
                 .unregisterTaskStackListener(mFrozenTaskListener);
@@ -210,15 +321,20 @@
         runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(listener));
     }
 
+    /**
+     * Adds a listener for the one handed mode change,
+     * guaranteed to be called after the device state's mode has changed.
+     */
+    public void addOneHandedModeChangedCallback(OneHandedModeChangeListener listener) {
+        listener.onOneHandedModeChanged(mSysUiNavMode.addOneHandedOverlayChangeListener(listener));
+        runOnDestroy(() -> mSysUiNavMode.removeOneHandedOverlayChangeListener(listener));
+    }
+
     @Override
     public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
-        }
         mDefaultDisplay.removeChangeListener(this);
-        if (newMode.hasGestures) {
-            mDefaultDisplay.addChangeListener(this);
-        }
+        mDefaultDisplay.addChangeListener(this);
+        onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
 
         if (newMode == NO_BUTTON) {
             mExclusionListener.register();
@@ -228,7 +344,8 @@
 
         mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
 
-        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
+        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo(),
+                mContext.getApplicationContext().getResources());
         if (!mMode.hasGestures && newMode.hasGestures) {
             setupOrientationSwipeHandler();
         } else if (mMode.hasGestures && !newMode.hasGestures){
@@ -240,14 +357,39 @@
 
     @Override
     public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
-        if (info.id != getDisplayId()) {
+        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();
+        }
+    }
+
+    @Override
+    public void onOneHandedModeChanged(int newGesturalHeight) {
+        mOrientationTouchTransformer.setGesturalHeight(newGesturalHeight, mDefaultDisplay.getInfo(),
+                mContext.getApplicationContext().getResources());
     }
 
     /**
@@ -311,6 +453,13 @@
         return mIsUserUnlocked;
     }
 
+    /**
+     * @return whether the user has completed setup wizard
+     */
+    public boolean isUserSetupComplete() {
+        return mIsUserSetupComplete;
+    }
+
     private void notifyUserUnlocked() {
         for (Runnable action : mUserUnlockedActions) {
             action.run();
@@ -354,10 +503,11 @@
      * @return whether SystemUI is in a state where we can start a system gesture.
      */
     public boolean canStartSystemGesture() {
-        return (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
+        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_BUBBLES_EXPANDED) == 0
                 && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
                         || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
     }
@@ -378,6 +528,20 @@
     }
 
     /**
+     * @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() {
@@ -440,6 +604,13 @@
     }
 
     /**
+     * @return whether screen pinning is enabled and active
+     */
+    public boolean isOneHandedModeActive() {
+        return (mSystemUiStateFlags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
+    }
+
+    /**
      * Sets the region in screen space where the gestures should be deferred (ie. due to specific
      * nav bar ui).
      */
@@ -490,20 +661,48 @@
 
     /**
      * @param ev An ACTION_DOWN motion event
-     * @return whether the given motion event can trigger the assistant.
+     * @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) {
+    public boolean canTriggerAssistantAction(MotionEvent ev, ActivityManager.RunningTaskInfo task) {
         return mAssistantAvailable
                 && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
                 && mOrientationTouchTransformer.touchInAssistantRegion(ev)
-                && !isLockToAppActive();
+                && !isLockToAppActive()
+                && !isGestureBlockedActivity(task);
+    }
+
+    /**
+     * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+     *
+     * @param ev The touch screen motion event.
+     * @return whether the given motion event can trigger the one handed mode.
+     */
+    public boolean canTriggerOneHandedAction(MotionEvent ev) {
+        if (!mIsOneHandedModeEnabled && !mIsSwipeToNotificationEnabled) {
+            return false;
+        }
+
+        final DefaultDisplay.Info displayInfo = mDefaultDisplay.getInfo();
+        return (mOrientationTouchTransformer.touchInOneHandedModeRegion(ev)
+                && displayInfo.rotation != Surface.ROTATION_90
+                && displayInfo.rotation != Surface.ROTATION_270
+                && displayInfo.metrics.densityDpi < DisplayMetrics.DENSITY_600);
+    }
+
+    public boolean isOneHandedModeEnabled() {
+        return mIsOneHandedModeEnabled;
+    }
+
+    public boolean isSwipeToNotificationEnabled() {
+        return mIsSwipeToNotificationEnabled;
     }
 
     /**
      * *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
      */
-    public void setOrientationTransformIfNeeded(MotionEvent event) {
+    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()));
@@ -511,11 +710,79 @@
         mOrientationTouchTransformer.transform(event);
     }
 
-    public void enableMultipleRegions(boolean enable) {
+    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();
     }
 
@@ -535,6 +802,8 @@
         pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
         pw.println("  displayRotation=" + getDisplayRotation());
         pw.println("  isUserUnlocked=" + mIsUserUnlocked);
+        pw.println("  isOneHandedModeEnabled=" + mIsOneHandedModeEnabled);
+        pw.println("  isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled);
         mOrientationTouchTransformer.dump(pw);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
index 5fa6bc7..ab5e3ba 100644
--- a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
@@ -16,19 +16,16 @@
 package com.android.quickstep;
 
 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;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * Holds a collection of RemoteAnimationTargets, filtered by different properties.
  */
 public class RemoteAnimationTargets {
 
-    private final Queue<SyncRtSurfaceTransactionApplierCompat> mDependentTransactionAppliers =
-            new ArrayDeque<>(1);
+    private final CopyOnWriteArrayList<ReleaseCheck> mReleaseChecks = new CopyOnWriteArrayList<>();
 
     public final RemoteAnimationTargetCompat[] unfilteredApps;
     public final RemoteAnimationTargetCompat[] apps;
@@ -36,6 +33,8 @@
     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<>();
@@ -68,7 +67,7 @@
     }
 
     public boolean isAnimatingHome() {
-        for (RemoteAnimationTargetCompat target : apps) {
+        for (RemoteAnimationTargetCompat target : unfilteredApps) {
             if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
                 return true;
             }
@@ -76,21 +75,65 @@
         return false;
     }
 
-    public void addDependentTransactionApplier(SyncRtSurfaceTransactionApplierCompat delay) {
-        mDependentTransactionAppliers.add(delay);
+    public void addReleaseCheck(ReleaseCheck check) {
+        mReleaseChecks.add(check);
     }
 
     public void release() {
-        SyncRtSurfaceTransactionApplierCompat applier = mDependentTransactionAppliers.poll();
-        if (applier == null) {
-            for (RemoteAnimationTargetCompat target : unfilteredApps) {
-                target.release();
+        if (mReleased) {
+            return;
+        }
+        for (ReleaseCheck check : mReleaseChecks) {
+            if (!check.mCanRelease) {
+                check.addOnSafeToReleaseCallback(this::release);
+                return;
             }
-            for (RemoteAnimationTargetCompat target : wallpapers) {
-                target.release();
+        }
+        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();
             }
-        } else {
-            applier.addAfterApplyCallback(this::release);
+        }
+
+        /**
+         * 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 375e589..6994d66 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,16 +16,19 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.util.Log;
 
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -56,15 +59,19 @@
             new MainThreadInitializedObject<>(SysUINavigationMode::new);
 
     private static final String TAG = "SysUINavigationMode";
-
     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String NAV_BAR_INTERACTION_MODE_RES_NAME =
             "config_navBarInteractionMode";
+    private static final String TARGET_OVERLAY_PACKAGE = "android";
 
     private final Context mContext;
     private Mode mMode;
 
+    private int mNavBarGesturalHeight;
+
     private final List<NavigationModeChangeListener> mChangeListeners = new ArrayList<>();
+    private final List<OneHandedModeChangeListener> mOneHandedOverlayChangeListeners =
+            new ArrayList<>();
 
     public SysUINavigationMode(Context context) {
         mContext = context;
@@ -73,18 +80,50 @@
         mContext.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
-                Mode oldMode = mMode;
-                initializeMode();
-                if (mMode != oldMode) {
-                    dispatchModeChange();
-                }
+                updateMode();
+                updateGesturalHeight();
             }
-        }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
+        }, getPackageFilter(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED));
+    }
+
+    /** Updates navigation mode when needed. */
+    public void updateMode() {
+        Mode oldMode = mMode;
+        initializeMode();
+        if (mMode != oldMode) {
+            dispatchModeChange();
+        }
+    }
+
+    private void updateGesturalHeight() {
+        int newGesturalHeight = ResourceUtils.getDimenByName(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+                INVALID_RESOURCE_HANDLE);
+
+        if (newGesturalHeight == INVALID_RESOURCE_HANDLE) {
+            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+            return;
+        }
+
+        if (mNavBarGesturalHeight != newGesturalHeight) {
+            mNavBarGesturalHeight = newGesturalHeight;
+            dispatchOneHandedOverlayChange();
+        }
     }
 
     private void initializeMode() {
-        int modeInt = getSystemIntegerRes(mContext, NAV_BAR_INTERACTION_MODE_RES_NAME);
-        for(Mode m : Mode.values()) {
+        int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME,
+                mContext.getResources(), INVALID_RESOURCE_HANDLE);
+        mNavBarGesturalHeight = ResourceUtils.getDimenByName(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+                INVALID_RESOURCE_HANDLE);
+
+        if (modeInt == INVALID_RESOURCE_HANDLE) {
+            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+            return;
+        }
+
+        for (Mode m : Mode.values()) {
             if (m.resValue == modeInt) {
                 mMode = m;
             }
@@ -97,6 +136,12 @@
         }
     }
 
+    private void dispatchOneHandedOverlayChange() {
+        for (OneHandedModeChangeListener listener : mOneHandedOverlayChangeListeners) {
+            listener.onOneHandedModeChanged(mNavBarGesturalHeight);
+        }
+    }
+
     public Mode addModeChangeListener(NavigationModeChangeListener listener) {
         mChangeListeners.add(listener);
         return mMode;
@@ -106,20 +151,17 @@
         mChangeListeners.remove(listener);
     }
 
-    public Mode getMode() {
-        return mMode;
+    public int addOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+        mOneHandedOverlayChangeListeners.add(listener);
+        return mNavBarGesturalHeight;
     }
 
-    private static int getSystemIntegerRes(Context context, String resName) {
-        Resources res = context.getResources();
-        int resId = res.getIdentifier(resName, "integer", "android");
+    public void removeOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+        mOneHandedOverlayChangeListeners.remove(listener);
+    }
 
-        if (resId != 0) {
-            return res.getInteger(resId);
-        } else {
-            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
-            return -1;
-        }
+    public Mode getMode() {
+        return mMode;
     }
 
     /** @return Whether we can remove the shelf from overview. */
@@ -128,8 +170,25 @@
         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());
+        pw.println("  mNavBarGesturalHeight=:" + mNavBarGesturalHeight);
+    }
+
     public interface NavigationModeChangeListener {
 
         void onNavigationModeChanged(Mode newMode);
     }
+
+    public interface OneHandedModeChangeListener {
+
+        void onOneHandedModeChanged(int newGesturalHeight);
+    }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index f5aaf1d..5b239a4 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -31,6 +31,7 @@
 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.
@@ -334,6 +335,7 @@
         }
     }
 
+    @Override
     public void onQuickSwitchToNewTask(int rotation) {
         if (mSystemUiProxy != null) {
             try {
@@ -343,4 +345,50 @@
             }
         }
     }
+
+    @Override
+    public void startOneHandedMode() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startOneHandedMode();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startOneHandedMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void stopOneHandedMode() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.stopOneHandedMode();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stopOneHandedMode", 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");
+            }
+        }
+    }
+
+    @Override
+    public void expandNotificationPanel() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.expandNotificationPanel();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call expandNotificationPanel", e);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index b04a1ae..cad51f4 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -18,6 +18,7 @@
 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;
@@ -26,9 +27,9 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.testing.TestProtocol;
 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 {
 
@@ -37,6 +38,7 @@
     private RecentsAnimationTargets mTargets;
     // Temporary until we can hook into gesture state events
     private GestureState mLastGestureState;
+    private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
 
     /**
      * Preloads the recents animation.
@@ -53,9 +55,6 @@
     @UiThread
     public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
             Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_FROM_RECENTS, "startRecentsAnimation");
-        }
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -70,13 +69,21 @@
 
         final BaseActivityInterface activityInterface = gestureState.getActivityInterface();
         mLastGestureState = gestureState;
-        mCallbacks = new RecentsAnimationCallbacks(activityInterface.shouldMinimizeSplitScreen());
+        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
@@ -94,6 +101,20 @@
             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);
@@ -110,6 +131,9 @@
         mCallbacks.removeListener(mLastGestureState);
         mLastGestureState = gestureState;
         mCallbacks.addListener(gestureState);
+        gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED
+                | STATE_RECENTS_ANIMATION_STARTED);
+        gestureState.updateLastAppearedTaskTarget(mLastAppearedTaskTarget);
         return mCallbacks;
     }
 
@@ -169,6 +193,7 @@
         mCallbacks = null;
         mTargets = null;
         mLastGestureState = null;
+        mLastAppearedTaskTarget = null;
     }
 
     public void dump() {
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index ace6743..2b7a8ec 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -166,11 +166,13 @@
             public void run() {
                 ThumbnailData thumbnail = ActivityManagerWrapper.getInstance().getTaskThumbnail(
                         key.id, lowResolution);
-                if (isCanceled()) {
-                    // We don't call back to the provided callback in this case
-                    return;
-                }
+
                 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();
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/BackGestureTutorialConfirmController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
deleted file mode 100644
index 486d676..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-
-import java.util.Optional;
-
-/**
- * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
- * {@link TutorialStep#CONFIRM}.
- */
-final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
-
-    BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
-            BackGestureTutorialTypeInfo tutorialTypeInfo) {
-        super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
-    }
-
-    @Override
-    Optional<Integer> getTitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
-    }
-
-    @Override
-    Optional<Integer> getSubtitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
-    }
-
-    @Override
-    Optional<Integer> getActionButtonStringId() {
-        return Optional.of(R.string.back_gesture_tutorial_action_button_label);
-    }
-
-    @Override
-    Optional<Integer> getActionTextButtonStringId() {
-        return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
-    }
-
-    @Override
-    void onActionButtonClicked(View button) {
-        hideHandCoachingAnimation();
-        if (button == mActionTextButton) {
-            mFragment.startSystemNavigationSetting();
-        }
-        mFragment.closeTutorial();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 5c2e992..921e568 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -15,164 +15,143 @@
  */
 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 android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
 
-import java.util.Optional;
+/** A {@link TutorialController} for the Back tutorial. */
+final class BackGestureTutorialController extends TutorialController {
 
-/**
- * Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
- */
-abstract class BackGestureTutorialController {
-
-    final BackGestureTutorialFragment mFragment;
-    final TutorialStep mTutorialStep;
-    final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
-    final Button mActionTextButton;
-    final Button mActionButton;
-    final TextView mSubtitleTextView;
-    final ImageButton mCloseButton;
-    final BackGestureTutorialHandAnimation mHandCoachingAnimation;
-    final LinearLayout mTitlesContainer;
-
-    private final TextView mTitleTextView;
-    private final ImageView mHandCoachingView;
-
-    BackGestureTutorialController(
-            BackGestureTutorialFragment fragment,
-            TutorialStep tutorialStep,
-            Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
-        mFragment = fragment;
-        mTutorialStep = tutorialStep;
-        mTutorialTypeInfo = tutorialTypeInfo;
-
-        View rootView = fragment.getRootView();
-        mActionTextButton = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_action_text_button);
-        mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
-        mSubtitleTextView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_subtitle_view);
-        mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
-        mHandCoachingView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_hand_coaching);
-        mHandCoachingAnimation = mFragment.getHandAnimation();
-        mHandCoachingView.bringToFront();
-        mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
-        mTitlesContainer = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_titles_container);
+    BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
+        super(fragment, tutorialType);
     }
 
-    void transitToController() {
-        updateTitles();
-        updateActionButtons();
-    }
-
-    void hideHandCoachingAnimation() {
-        mHandCoachingAnimation.stop();
-    }
-
-    void onGestureAttempted(BackGestureResult result) {
-        if (mTutorialStep == TutorialStep.CONFIRM
-                && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
-                    || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
-            mFragment.closeTutorial();
-            return;
-        }
-
-        if (!mTutorialTypeInfo.isPresent()) {
-            return;
-        }
-
-        switch (mTutorialTypeInfo.get().getTutorialType()) {
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
             case RIGHT_EDGE_BACK_NAVIGATION:
-                if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
-                    hideHandCoachingAnimation();
-                    mFragment.changeController(
-                            TutorialStep.ENGAGED, TutorialType.LEFT_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:
-                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
-                    hideHandCoachingAnimation();
-                    mFragment.changeController(TutorialStep.CONFIRM);
+                handleAttemptFromLeft(result);
+                break;
+            case BACK_NAVIGATION_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
                 }
                 break;
         }
     }
 
-    abstract Optional<Integer> getTitleStringId();
-
-    abstract Optional<Integer> getSubtitleStringId();
-
-    abstract Optional<Integer> getActionButtonStringId();
-
-    abstract Optional<Integer> getActionTextButtonStringId();
-
-    abstract void onActionButtonClicked(View button);
-
-    private void updateActionButtons() {
-        updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
-        updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
-    }
-
-    private static void updateButton(Button button, Optional<Integer> stringId,
-            View.OnClickListener listener) {
-        if (!stringId.isPresent()) {
-            button.setVisibility(View.INVISIBLE);
-            return;
+    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;
         }
-
-        button.setVisibility(View.VISIBLE);
-        button.setText(stringId.get());
-        button.setOnClickListener(listener);
     }
 
-    private void updateTitles() {
-        updateTitleView(mTitleTextView, getTitleStringId(),
-                R.style.TextAppearance_BackGestureTutorial_Title);
-        updateTitleView(mSubtitleTextView, getSubtitleStringId(),
-                R.style.TextAppearance_BackGestureTutorial_Subtitle);
-    }
-
-    private static void updateTitleView(TextView textView, Optional<Integer> stringId,
-            int styleId) {
-        if (!stringId.isPresent()) {
-            textView.setVisibility(View.GONE);
-            return;
+    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;
         }
-
-        textView.setVisibility(View.VISIBLE);
-        textView.setText(stringId.get());
-        textView.setTextAppearance(styleId);
     }
 
-    /**
-     * Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
-     * {@link TutorialStep}.
-     */
-    static Optional<BackGestureTutorialController> getTutorialController(
-            BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
-            TutorialType tutorialType) {
-        BackGestureTutorialTypeInfo tutorialTypeInfo =
-                BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
-        switch (tutorialStep) {
-            case ENGAGED:
-                return Optional.of(
-                        new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
-            case CONFIRM:
-                return Optional.of(
-                        new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
-            default:
-                throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
+    @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/BackGestureTutorialEngagedController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
deleted file mode 100644
index c9ee1e2..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.view.View;
-
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-
-import java.util.Optional;
-
-/**
- * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
- * {@link TutorialStep#ENGAGED}.
- */
-final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
-
-    BackGestureTutorialEngagedController(
-            BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
-        super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
-    }
-
-    @Override
-    void transitToController() {
-        super.transitToController();
-        mHandCoachingAnimation.maybeStartLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
-    }
-
-    @Override
-    Optional<Integer> getTitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
-    }
-
-    @Override
-    Optional<Integer> getSubtitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
-    }
-
-    @Override
-    Optional<Integer> getActionButtonStringId() {
-        return Optional.empty();
-    }
-
-    @Override
-    Optional<Integer> getActionTextButtonStringId() {
-        return Optional.empty();
-    }
-
-    @Override
-    void onActionButtonClicked(View button) {
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index aeb718d..bef50ea 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -15,172 +15,34 @@
  */
 package com.android.quickstep.interaction;
 
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.graphics.Insets;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
-
-import java.net.URISyntaxException;
-import java.util.Optional;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 /** Shows the Back gesture interactive tutorial. */
-public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
-
-    private static final String LOG_TAG = "TutorialFragment";
-    private static final String KEY_TUTORIAL_STEP = "tutorialStep";
-    private static final String KEY_TUTORIAL_TYPE = "tutorialType";
-    private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
-            "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
-                    + ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
-                    + ".:settings:show_fragment=com.android.settings.gestures"
-                    + ".SystemNavigationGestureSettings;end";
-
-    private TutorialStep mTutorialStep;
-    private TutorialType mTutorialType;
-    private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
-    private View mRootView;
-    private BackGestureTutorialHandAnimation mHandCoachingAnimation;
-    private EdgeBackGestureHandler mEdgeBackGestureHandler;
-
-    public static BackGestureTutorialFragment newInstance(
-            TutorialStep tutorialStep, TutorialType tutorialType) {
-        BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
-        Bundle args = new Bundle();
-        args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
-        args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
-        fragment.setArguments(args);
-        return fragment;
+public class BackGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.back_gesture;
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
-        mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
-        mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
-        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
-        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
+    TutorialController createController(TutorialType type) {
+        return new BackGestureTutorialController(this, type);
     }
 
     @Override
-    public View onCreateView(
-            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        super.onCreateView(inflater, container, savedInstanceState);
-
-        mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
-                container, /* attachToRoot= */ false);
-        mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
-                .setOnClickListener(this::onCloseButtonClicked);
-        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
-            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
-            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
-            return insets;
-        });
-        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
-        mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
-
-        return mRootView;
+    Class<? extends TutorialController> getControllerClass() {
+        return BackGestureTutorialController.class;
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        changeController(mTutorialStep, mTutorialType);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        mHandCoachingAnimation.stop();
-    }
-
-    void onAttachedToWindow() {
-        mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
-    }
-
-    void onDetachedFromWindow() {
-        mEdgeBackGestureHandler.setViewGroupParent(null);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle savedInstanceState) {
-        savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
-        savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
-        super.onSaveInstanceState(savedInstanceState);
-    }
-
-    View getRootView() {
-        return mRootView;
-    }
-
-    BackGestureTutorialHandAnimation getHandAnimation() {
-        return mHandCoachingAnimation;
-    }
-
-    void changeController(TutorialStep tutorialStep) {
-        changeController(tutorialStep, mTutorialType);
-    }
-
-    void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
-        Optional<BackGestureTutorialController> tutorialController =
-                BackGestureTutorialController.getTutorialController(/* fragment= */ this,
-                        tutorialStep, tutorialType);
-        if (!tutorialController.isPresent()) {
-            return;
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
         }
-
-        mTutorialController = tutorialController;
-        mTutorialController.get().transitToController();
-        this.mTutorialStep = mTutorialController.get().mTutorialStep;
-        this.mTutorialType = tutorialType;
-    }
-
-    @Override
-    public void onBackGestureAttempted(BackGestureResult result) {
-        mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
-    }
-
-    void closeTutorial() {
-        getActivity().finish();
-    }
-
-    void startSystemNavigationSetting() {
-        try {
-            startActivityForResult(
-                    Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
-                    /* requestCode= */ 0);
-        } catch (URISyntaxException e) {
-            Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
-        } catch (ActivityNotFoundException e) {
-            Log.e(LOG_TAG, "The launch Activity not found: " + e);
-        }
-    }
-
-    private void onCloseButtonClicked(View button) {
-        closeTutorial();
-    }
-
-    /** Denotes the step of the tutorial. */
-    enum TutorialStep {
-        ENGAGED,
-        CONFIRM,
-    }
-
-    /** Denotes the type of the tutorial. */
-    enum TutorialType {
-        RIGHT_EDGE_BACK_NAVIGATION,
-        LEFT_EDGE_BACK_NAVIGATION,
+        return super.onTouch(view, motionEvent);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
deleted file mode 100644
index ac8443d..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
-
-/** Defines the UI element identifiers for the particular {@link TutorialType}. */
-final class BackGestureTutorialTypeInfo {
-
-    private final TutorialType mTutorialType;
-    private final int mTutorialPlaygroundTitleId;
-    private final int mTutorialEngagedSubtitleId;
-    private final int mTutorialConfirmTitleId;
-    private final int mTutorialConfirmSubtitleId;
-
-    TutorialType getTutorialType() {
-        return mTutorialType;
-    }
-
-    int getTutorialPlaygroundTitleId() {
-        return mTutorialPlaygroundTitleId;
-    }
-
-    int getTutorialEngagedSubtitleId() {
-        return mTutorialEngagedSubtitleId;
-    }
-
-    int getTutorialConfirmTitleId() {
-        return mTutorialConfirmTitleId;
-    }
-
-    int getTutorialConfirmSubtitleId() {
-        return mTutorialConfirmSubtitleId;
-    }
-
-    static Builder builder() {
-        return new Builder();
-    }
-
-    private BackGestureTutorialTypeInfo(
-            TutorialType tutorialType,
-            int tutorialPlaygroundTitleId,
-            int tutorialEngagedSubtitleId,
-            int tutorialConfirmTitleId,
-            int tutorialConfirmSubtitleId) {
-        mTutorialType = tutorialType;
-        mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
-        mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
-        mTutorialConfirmTitleId = tutorialConfirmTitleId;
-        mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
-    }
-
-    /** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
-    static class Builder {
-
-        private TutorialType mTutorialType;
-        private Integer mTutorialPlaygroundTitleId;
-        private Integer mTutorialEngagedSubtitleId;
-        private Integer mTutorialConfirmTitleId;
-        private Integer mTutorialConfirmSubtitleId;
-
-        Builder setTutorialType(TutorialType tutorialType) {
-            mTutorialType = tutorialType;
-            return this;
-        }
-
-        Builder setTutorialPlaygroundTitleId(int stringId) {
-            mTutorialPlaygroundTitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialEngagedSubtitleId(int stringId) {
-            mTutorialEngagedSubtitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialConfirmTitleId(int stringId) {
-            mTutorialConfirmTitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialConfirmSubtitleId(int stringId) {
-            mTutorialConfirmSubtitleId = stringId;
-            return this;
-        }
-
-        BackGestureTutorialTypeInfo build() {
-            return new BackGestureTutorialTypeInfo(
-                    mTutorialType,
-                    mTutorialPlaygroundTitleId,
-                    mTutorialEngagedSubtitleId,
-                    mTutorialConfirmTitleId,
-                    mTutorialConfirmSubtitleId);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
deleted file mode 100644
index 9575d83..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
-
-/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
-final class BackGestureTutorialTypeInfoProvider {
-
-    private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
-            BackGestureTutorialTypeInfo.builder()
-                    .setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
-                    .setTutorialPlaygroundTitleId(
-                            R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
-                    .setTutorialEngagedSubtitleId(
-                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
-                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
-                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
-                    .build();
-
-    private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
-            BackGestureTutorialTypeInfo.builder()
-                    .setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
-                    .setTutorialPlaygroundTitleId(
-                            R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
-                    .setTutorialEngagedSubtitleId(
-                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
-                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
-                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
-                    .build();
-
-    static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
-        switch (tutorialType) {
-            case RIGHT_EDGE_BACK_NAVIGATION:
-                return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
-            case LEFT_EDGE_BACK_NAVIGATION:
-                return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
-            default:
-                throw new AssertionError("Unexpected tutorial type: " + tutorialType);
-        }
-    }
-
-    private BackGestureTutorialTypeInfoProvider() {
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index f34530e..e4b348e 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -20,7 +20,6 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.os.SystemProperties;
-import android.view.Display;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -46,7 +45,6 @@
     private final Context mContext;
 
     private final Point mDisplaySize = new Point();
-    private final int mDisplayId;
 
     // The edge width where touch down is allowed
     private int mEdgeWidth;
@@ -60,6 +58,7 @@
     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;
@@ -91,8 +90,6 @@
     EdgeBackGestureHandler(Context context) {
         final Resources res = context.getResources();
         mContext = context;
-        mDisplayId = context.getDisplay() == null
-                ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();
 
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
@@ -126,6 +123,10 @@
         mGestureCallback = callback;
     }
 
+    void unregisterBackGestureAttemptCallback() {
+        mGestureCallback = null;
+    }
+
     private LayoutParams createLayoutParams() {
         Resources resources = mContext.getResources();
         return new LayoutParams(
@@ -145,11 +146,13 @@
     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;
         }
 
@@ -169,12 +172,12 @@
         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);
-
-                mDownPoint.set(ev.getX(), ev.getY());
                 mThresholdCrossed = false;
             }
         } else if (mAllowGesture) {
@@ -193,7 +196,6 @@
                     if (dy > dx && dy > mTouchSlop) {
                         cancelGesture(ev);
                         return;
-
                     } else if (dx > dy && dx > mTouchSlop) {
                         mThresholdCrossed = true;
                     }
@@ -206,8 +208,10 @@
         }
 
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
-            if (!mAllowGesture && mGestureCallback != null) {
-                mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
+            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);
             }
         }
     }
@@ -223,7 +227,8 @@
         BACK_COMPLETED_FROM_RIGHT,
         BACK_CANCELLED_FROM_LEFT,
         BACK_CANCELLED_FROM_RIGHT,
-        BACK_NOT_STARTED,
+        BACK_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        BACK_NOT_STARTED_IN_NAV_BAR_REGION,
     }
 
     /** Callback to let the UI react to attempted back gestures. */
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
index 5bf5026..0521db4 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -280,7 +280,11 @@
                 new SpringForce()
                         .setStiffness(SpringForce.STIFFNESS_MEDIUM)
                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
-        mPaint.setColor(context.getColor(R.color.back_arrow_color_dark));
+        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();
 
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 4815366..f8d9d8d 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -15,6 +15,8 @@
  */
 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;
@@ -26,26 +28,26 @@
 import androidx.fragment.app.FragmentActivity;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 import java.util.List;
 
 /** Shows the gesture interactive sandbox in full screen mode. */
 public class GestureSandboxActivity extends FragmentActivity {
 
-    private BackGestureTutorialFragment mFragment;
+    private static final String LOG_TAG = "GestureSandboxActivity";
+
+    private TutorialFragment mFragment;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         requestWindowFeature(Window.FEATURE_NO_TITLE);
-        setContentView(R.layout.back_gesture_tutorial_activity);
+        setContentView(R.layout.gesture_tutorial_activity);
 
-        mFragment = BackGestureTutorialFragment.newInstance(
-            TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
+        mFragment = TutorialFragment.newInstance(getTutorialType(getIntent().getExtras()));
         getSupportFragmentManager().beginTransaction()
-                .add(R.id.back_gesture_tutorial_fragment_container, mFragment)
+                .add(R.id.gesture_tutorial_fragment_container, mFragment)
                 .commit();
     }
 
@@ -70,6 +72,19 @@
         }
     }
 
+    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
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/BackGestureTutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
similarity index 68%
rename from quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
rename to quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
index d03811d..c810e43 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
@@ -25,12 +25,12 @@
 import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 import java.time.Duration;
 
 /** Hand coaching animation. */
-final class BackGestureTutorialHandAnimation {
+final class TutorialHandAnimation {
 
     // A delay for waiting the Activity fully launches.
     private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
@@ -38,29 +38,19 @@
     private final ImageView mHandCoachingView;
     private final AnimatedVectorDrawable mGestureAnimation;
 
-    private boolean mIsAnimationPlayed = false;
-
-    BackGestureTutorialHandAnimation(Context context, View rootView) {
-        mHandCoachingView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_hand_coaching);
-        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
-                R.drawable.back_gesture);
+    TutorialHandAnimation(Context context, View rootView, int resId) {
+        mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
+        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context, resId);
     }
 
-    boolean isRunning() {
-        return mGestureAnimation.isRunning();
-    }
-
-    /**
-     * Starts animation if the playground is launched for the first time.
-     */
-    void maybeStartLoopedAnimation(TutorialType tutorialType) {
-        if (isRunning() || mIsAnimationPlayed) {
-            return;
+    /** [Re]starts animation for the given tutorial. */
+    void startLoopedAnimation(TutorialType tutorialType) {
+        mHandCoachingView.setVisibility(View.VISIBLE);
+        if (mGestureAnimation.isRunning()) {
+            stop();
         }
 
-        mIsAnimationPlayed = true;
-        clearAnimationCallbacks();
+        mGestureAnimation.clearAnimationCallbacks();
         mGestureAnimation.registerAnimationCallback(
                 new Animatable2.AnimationCallback() {
                     @Override
@@ -78,17 +68,11 @@
         float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
         mHandCoachingView.setRotationY(rotationY);
         mHandCoachingView.setImageDrawable(mGestureAnimation);
-        mHandCoachingView.postDelayed(() -> mGestureAnimation.start(),
-                ANIMATION_START_DELAY.toMillis());
-    }
-
-    private void clearAnimationCallbacks() {
-        mGestureAnimation.clearAnimationCallbacks();
+        mHandCoachingView.postDelayed(mGestureAnimation::start, ANIMATION_START_DELAY.toMillis());
     }
 
     void stop() {
-        mIsAnimationPlayed = false;
-        clearAnimationCallbacks();
+        mGestureAnimation.clearAnimationCallbacks();
         mGestureAnimation.stop();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 58bb980..eac45e9 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,91 +16,437 @@
 
 package com.android.quickstep.logging;
 
-import static android.stats.launcher.nano.Launcher.ALLAPPS;
-import static android.stats.launcher.nano.Launcher.BACKGROUND;
-import static android.stats.launcher.nano.Launcher.HOME;
-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.util.Log;
 
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
+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.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 = "StatsLog";
-    private static final boolean DEBUG = false;
+    private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
+
     private static Context sContext;
 
+    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 log(LauncherEvent eventId, LauncherAtom.ItemInfo item) {
-        // Call StatsLog method
+    public StatsLogger logger() {
+        return new StatsCompatLogger();
     }
 
+    /**
+     * Logs a ranking event and accompanying {@link InstanceId} and package name.
+     */
     @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");
-        }
+    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 logSnapshot() {
         LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
                 new SnapshotWorker());
     }
 
     private class SnapshotWorker extends BaseModelUpdateTask {
+        private final InstanceId mInstanceId;
+        SnapshotWorker() {
+            mInstanceId = new InstanceIdSequence(
+                    1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
+        }
+
         @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, null);
-                // call StatsLog method
+                LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
+                writeSnapshot(atomInfo, mInstanceId);
             }
             for (FolderInfo fInfo : folders) {
-                for (ItemInfo info : fInfo.contents) {
-                    LauncherAtom.ItemInfo atomInfo = info.buildProto(null, fInfo);
-                    // call StatsLog method
-                }
+                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, null);
-                // call StatsLog method
+                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;
+            }
+
+            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);
+                            }
+                        });
+            }
+        }
+
+        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 */);
+        }
+    }
+
+    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();
+        }
+    }
+
+    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:
+                return null;
+        }
+    }
+
+    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
index b1c72ce..dfb8c1d 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -28,9 +28,11 @@
 
 public class ActivityInitListener<T extends BaseActivity> implements SchedulerCallback<T> {
 
-    private final BiPredicate<T, Boolean> mOnInitListener;
+    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
@@ -43,27 +45,46 @@
     }
 
     @Override
-    public boolean init(T activity, boolean alreadyOnHome) {
+    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() {
-        mActivityTracker.schedule(this);
+    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() {
-        mActivityTracker.clearReference(this);
+        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) {
-        register();
+        mIsRegistered = true;
 
         Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
-        context.startActivity(addToIntent(new Intent((intent))), options);
+        context.startActivity(addToIntent(new Intent(intent)), options);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
index f5fbf28..e998e9a 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -19,6 +19,7 @@
 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;
@@ -39,6 +40,8 @@
 
 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;
@@ -53,14 +56,18 @@
 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, int taskId) {
-        systemUiProxy.handleImageAsScreenshot(screenshot, screenshotBounds, visibleInsets, taskId);
+            Insets visibleInsets, Task.TaskKey task) {
+        systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot),
+                screenshotBounds, visibleInsets, task);
     }
 
     /**
@@ -85,8 +92,14 @@
     @WorkerThread
     public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop,
             Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) {
-        context.startActivities(
-                uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent));
+        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);
+        }
     }
 
     /**
@@ -101,10 +114,13 @@
      */
     @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 = "image_" + bitmap.hashCode() + "_" + cropHash + ".png";
-        File file = new File(context.getCacheDir(), baseName);
+        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);
@@ -155,15 +171,30 @@
             intent = new Intent();
         }
         ClipData clipdata = new ClipData(new ClipDescription("content",
-                new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
+                new String[]{"image/png"}),
                 new ClipData.Item(uri));
         intent.setAction(Intent.ACTION_SEND)
                 .setComponent(null)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 .setType("image/png")
-                .setFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 .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 1f1a999..ae19d73 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -18,30 +18,19 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
 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
      */
@@ -53,107 +42,13 @@
         return swipeHeight;
     }
 
-    public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        float extraSpace;
-        if (dp.isVerticalBarLayout()) {
-            extraSpace = 0;
-        } else {
-            Resources res = context.getResources();
-
-            if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
-                //TODO: this needs to account for the swipe gesture height and accessibility
-                // UI when shown.
-                extraSpace = 0;
-            } else {
-                extraSpace = getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
-                        + 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();
-        final boolean overviewActionsEnabled = ENABLE_OVERVIEW_ACTIONS.get();
-
-        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;
-
-            final int paddingResId;
-            if (dp.isVerticalBarLayout()) {
-                paddingResId = R.dimen.landscape_task_card_horz_space;
-            } else if (overviewActionsEnabled && removeShelfFromOverview(context)) {
-                paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
-            } else {
-                paddingResId = R.dimen.portrait_task_card_horz_space;
-            }
-            paddingHorz = res.getDimension(paddingResId);
-        }
-
-        float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
-        float bottomMargin = thumbnailBottomMargin(context);
-
-        float paddingVert = overviewActionsEnabled && removeShelfFromOverview(context)
-                ? 0 : 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 - bottomMargin;
-        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 - bottomMargin) / 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();
-            calculateLauncherTaskSize(context, dp, taskSize);
+            LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
+                    orientationHandler);
             return (dp.heightPx - taskSize.height()) / 2;
         }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
@@ -163,14 +58,16 @@
     }
 
     /**
-     * Get the margin that the task thumbnail view should use.
-     * @return the margin in pixels.
+     * Recursively sets view and all children enabled/disabled.
+     * @param view Top most parent view to change.
+     * @param enabled True = enable, False = disable.
      */
-    public static int thumbnailBottomMargin(Context context) {
-        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
-            return context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height);
-        } else {
-            return 0;
+    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 7d52571..a5d4568 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -19,11 +19,13 @@
 
 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
@@ -84,6 +86,9 @@
         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;
@@ -120,6 +125,9 @@
      * @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);
@@ -167,6 +175,9 @@
     }
 
     private void updatePaused(boolean isPaused) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "updatePaused: " + isPaused);
+        }
         if (mDisallowPause) {
             isPaused = false;
         }
@@ -188,6 +199,9 @@
         setOnMotionPauseListener(null);
         mIsPaused = mHasEverBeenPaused = false;
         mSlowStartTime = 0;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "canceling alarm");
+        }
         mForcePauseTimeout.cancelAlarm();
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
index 74e6b29..0a98e1b 100644
--- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -35,6 +35,11 @@
         mDisplayRotation = info.rotation;
     }
 
+    public NavBarPosition(SysUINavigationMode.Mode mode, int displayRotation) {
+        mMode = mode;
+        mDisplayRotation = displayRotation;
+    }
+
     public boolean isRightEdge() {
         return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
     }
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 21b97ec..04308c8 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -25,12 +25,10 @@
 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;
 
 public abstract class RemoteAnimationProvider {
 
     LauncherAnimationRunner mAnimationRunner;
-    static final int Z_BOOST_BASE = 800570000;
 
     public abstract AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets);
@@ -53,27 +51,6 @@
     }
 
     /**
-     * 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.
-     */
-    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 int getLayer(RemoteAnimationTargetCompat target, int boostModeTarget) {
-        return target.mode == boostModeTarget
-                ? Z_BOOST_BASE + target.prefixOrderIndex
-                : target.prefixOrderIndex;
-    }
-
-    /**
      * @return the target with the lowest opaque layer for a certain app animation, or null.
      */
     public static RemoteAnimationTargetCompat findLowestOpaqueLayerTarget(
@@ -83,7 +60,7 @@
         for (int i = appTargets.length - 1; i >= 0; i--) {
             RemoteAnimationTargetCompat target = appTargets[i];
             if (target.mode == mode && !target.isTranslucent) {
-                int layer = getLayer(target, mode);
+                int layer = target.prefixOrderIndex;
                 if (layer < lowestLayer) {
                     lowestLayer = layer;
                     lowestLayerIndex = i;
diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
index fa2d338..958ee24 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
@@ -15,7 +15,6 @@
  */
 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;
@@ -42,7 +41,9 @@
     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/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index c2ccd90..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;
@@ -43,6 +44,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -75,6 +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;
@@ -100,6 +103,7 @@
     private boolean mRemainingScreenPathValid = false;
 
     private Mode mSysUINavigationMode;
+    private boolean mIsTwoZoneSwipeModel;
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -108,6 +112,7 @@
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mOnboardingPrefs = mLauncher.getOnboardingPrefs();
 
         // Just assume the easiest UI for now, until we have the proper layout information.
         mDrawingFlatColor = true;
@@ -140,9 +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;
         }
     }
 
@@ -181,6 +188,7 @@
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
         }
         updateColors();
+        updateSysUiColors();
         updateDragHandleAlpha();
         invalidate();
     }
@@ -235,10 +243,26 @@
     }
 
     @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 twoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
-                && SysUINavigationMode.removeShelfFromOverview(mLauncher);
-        return twoZoneSwipeModel || super.shouldDragHandleBeVisible();
+        boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
+                && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
+        return needsAllAppsEdu || super.shouldDragHandleBeVisible();
     }
 
     @Override
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index a726052..b9e0f62 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -37,7 +37,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -58,10 +57,8 @@
 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.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.RuleChain;
@@ -95,7 +92,8 @@
         mDevice = UiDevice.getInstance(instrumentation);
         mDevice.setOrientationNatural();
         mLauncher = new LauncherInstrumentation();
-        mLauncher.enableCheckEventsForSuccessfulGestures();
+        // b/143488140
+        //mLauncher.enableCheckEventsForSuccessfulGestures();
 
         if (TestHelpers.isInLauncherProcess()) {
             Utilities.enableRunningInTestHarnessForTests();
@@ -122,6 +120,9 @@
                     TestCommandReceiver.callCommand(TestCommandReceiver.DISABLE_TEST_LAUNCHER);
                     UiDevice.getInstance(getInstrumentation()).executeShellCommand(
                             getLauncherCommand(getLauncherInMyProcess()));
+                    // b/143488140
+                    mDevice.pressHome();
+                    mDevice.waitForIdle();
                 }
             }
         };
@@ -130,11 +131,15 @@
                     TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
                     getString("result"));
         }
+        // b/143488140
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
     }
 
-    @NavigationModeSwitch
+    // b/143488140
+    //@NavigationModeSwitch
     @Test
-    @Ignore // b/143488140
     public void goToOverviewFromHome() {
         mDevice.pressHome();
         assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg(
@@ -143,9 +148,9 @@
         mLauncher.getBackground().switchToOverview();
     }
 
-    @NavigationModeSwitch
+    // b/143488140
+    //@NavigationModeSwitch
     @Test
-    @Ignore // b/143488140
     public void goToOverviewFromApp() {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
 
@@ -178,9 +183,9 @@
         return mLauncher.getBackground().switchToOverview();
     }
 
-    @NavigationModeSwitch
+    // b/143488140
+    //@NavigationModeSwitch
     @Test
-    @Ignore // b/143488140
     public void testOverview() {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 0afe4a8..40265c4 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -203,7 +203,7 @@
                         + launcher.getNavigationModeMismatchError(),
                 () -> launcher.getNavigationModeMismatchError() == null,
                 60000 /* b/148422894 */, launcher);
-        AbstractLauncherUiTest.checkDetectedLeaks();
+        AbstractLauncherUiTest.checkDetectedLeaks(launcher);
         return true;
     }
 
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 731a220..9732cdc 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -25,7 +25,6 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.util.RaceConditionReproducer;
 import com.android.quickstep.NavigationModeSwitchRule.Mode;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -46,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));
     }
@@ -99,5 +100,6 @@
             // The test action.
             mLauncher.getBackground().switchToOverview();
         }
+        mLauncher.pressHome();
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 75fcfe2..bf093fd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -42,6 +42,7 @@
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.views.RecentsView;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,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 {
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index f8f22a1..115294a 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -17,10 +17,11 @@
 
 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.launcher3.ui.widget.BindWidgetTest.createWidgetInfo;
 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
 
 import static org.junit.Assert.assertEquals;
@@ -48,9 +49,9 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.LauncherAppWidgetInfo;
 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;
@@ -187,7 +188,7 @@
             LauncherSettings.Settings.call(mResolver,
                     LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
             LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-            LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+            LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
             addItemToScreen(item);
             assertTrue("Widget is not present",
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/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/res/layout/all_apps.xml b/res/layout/all_apps.xml
index a41fb9a..67ec664 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -19,6 +19,7 @@
 <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"
diff --git a/res/layout/all_apps_content_layout.xml b/res/layout/all_apps_content_layout.xml
new file mode 100644
index 0000000..5698977
--- /dev/null
+++ b/res/layout/all_apps_content_layout.xml
@@ -0,0 +1,27 @@
+<?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:id="@+id/apps_list_view_override"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    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/res/layout/arrow_toast.xml b/res/layout/arrow_toast.xml
index 087e45a..0ec9981 100644
--- a/res/layout/arrow_toast.xml
+++ b/res/layout/arrow_toast.xml
@@ -34,6 +34,8 @@
             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"
@@ -58,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 de13277..a137908 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -45,12 +45,8 @@
 
         <include
             android:id="@+id/overview_panel"
-            layout="@layout/overview_panel"
-            android:visibility="gone" />
+            layout="@layout/overview_panel" />
 
-        <include
-            android:id="@+id/overview_actions_view"
-            layout="@layout/overview_actions_container" />
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml
index 2637f03..f513688 100644
--- a/res/layout/overview_panel.xml
+++ b/res/layout/overview_panel.xml
@@ -17,4 +17,5 @@
 <Space
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="0dp"
-    android:layout_height="0dp" />
\ No newline at end of file
+    android:layout_height="0dp"
+    android:visibility="gone" />
\ No newline at end of file
diff --git a/res/layout/secondary_launcher.xml b/res/layout/secondary_launcher.xml
index 98cfc34..fdf4446 100644
--- a/res/layout/secondary_launcher.xml
+++ b/res/layout/secondary_launcher.xml
@@ -17,7 +17,8 @@
     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:id="@+id/drag_layer"
+    android:padding="@dimen/dynamic_grid_edge_margin">
 
     <GridView
         android:layout_width="match_parent"
@@ -119,8 +120,7 @@
             android:singleLine="true"
             android:textColor="?android:attr/textColorSecondary"
             android:textColorHint="@drawable/all_apps_search_hint"
-            android:textSize="16sp"
-            android:translationY="24dp" />
+            android:textSize="16sp" />
 
         <include layout="@layout/all_apps_fast_scroller" />
     </com.android.launcher3.allapps.AllAppsContainerView>
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/work_apps_paused.xml b/res/layout/work_apps_paused.xml
index f64b2d9..7f1107f 100644
--- a/res/layout/work_apps_paused.xml
+++ b/res/layout/work_apps_paused.xml
@@ -15,12 +15,13 @@
 <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/PrimaryMediumText"
+        style="@style/PrimaryHeadline"
         android:textColor="?attr/workProfileOverlayTextColor"
         android:id="@+id/work_apps_paused_title"
         android:layout_width="wrap_content"
@@ -34,6 +35,7 @@
     <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"
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
index 689dbe7..31953c7 100644
--- a/res/layout/work_mode_switch.xml
+++ b/res/layout/work_mode_switch.xml
@@ -17,7 +17,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    style="@style/PrimaryMediumText"
+    style="@style/PrimaryHeadline"
     android:id="@+id/work_mode_toggle"
     android:drawableStart="@drawable/ic_corp"
     android:drawablePadding="16dp"
@@ -25,14 +25,15 @@
     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="16sp"
+    android:textSize="@dimen/work_profile_footer_text_size"
     android:background="?attr/allAppsScrimColor"
     android:text="@string/work_profile_toggle_label"
-    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingLeft="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingRight="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_padding"
-/>
\ No newline at end of file
+    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
index 5506b94..c3c7010 100644
--- a/res/layout/work_profile_edu.xml
+++ b/res/layout/work_profile_edu.xml
@@ -23,13 +23,13 @@
         android:layout_width="match_parent"
         android:layout_height="32dp"
         android:background="@drawable/bottom_sheet_top_border"
-        android:backgroundTint="?android:attr/colorAccent" />
+        android:backgroundTint="?attr/eduHalfSheetBGColor" />
 
     <LinearLayout
         android:id="@+id/view_wrapper"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="?android:attr/colorAccent"
+        android:background="?attr/eduHalfSheetBGColor"
         android:orientation="vertical"
         android:paddingLeft="@dimen/bottom_sheet_edu_padding"
         android:paddingRight="@dimen/bottom_sheet_edu_padding">
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-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/res/values-v30/styles.xml b/res/values-v30/styles.xml
new file mode 100644
index 0000000..71740a9
--- /dev/null
+++ b/res/values-v30/styles.xml
@@ -0,0 +1,34 @@
+<?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>
+    <!-- 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/attrs.xml b/res/values/attrs.xml
index 5a15ec6..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,6 +35,8 @@
     <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" />
@@ -42,6 +45,7 @@
     <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">
@@ -118,6 +122,8 @@
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
         <attr name="dbFile" format="string" />
+        <!-- numAllAppsColumns defaults to numColumns, if not specified -->
+        <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
@@ -133,6 +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 194ef2c..043ad9a 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -38,11 +38,8 @@
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
 
-    <color name="back_gesture_tutorial_background_color">#FFFFFFFF</color>
-    <color name="back_gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
-    <color name="back_gesture_tutorial_title_color">#FF000000</color>
-    <color name="back_gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
-    <color name="back_gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
-
-
+    <color name="gesture_tutorial_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 ef67613..ca25325 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,8 +1,6 @@
 <resources>
     <!-- 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>
@@ -71,6 +69,7 @@
     <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>
+    <string name="prediction_model_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
@@ -125,14 +124,14 @@
     <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.5</item>
-    <item name="dismiss_task_trans_y_stiffness" type="dimen" format="float">1500</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.5</item>
-    <item name="dismiss_task_trans_x_stiffness" type="dimen" format="float">1500</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.75</item>
-    <item name="horizontal_spring_stiffness" type="dimen" format="float">200</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>
@@ -145,6 +144,10 @@
     <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>
@@ -176,6 +179,10 @@
 
         <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">
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0b589a2..947e635 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -96,7 +96,6 @@
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
 
-    <dimen name="all_apps_work_profile_tab_footer_padding">20dp</dimen>
 
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
@@ -105,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>
@@ -200,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>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ac04262..935bb40 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -103,9 +103,9 @@
     <!-- 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">Don\'t suggest app</string>
+    <string name="dismiss_prediction_label">Don\'t suggest app</string>
     <!-- Label for pinning predicted app. -->
-    <string name="pin_prediction" translatable="false">Pin Prediction</string>
+    <string name="pin_prediction">Pin Prediction</string>
 
 
     <!-- Permissions: -->
@@ -172,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 -->
@@ -182,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>
@@ -207,7 +209,7 @@
     <!-- Summary for Notification dots setting. Tapping this will link enable/disable notification dots feature on the home screen. [CHAR LIMIT=50] -->
     <string name="notification_dots_service_title">Show notification dots</string>
 
-    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=40] -->
+    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=60] -->
     <string name="auto_add_shortcuts_label">Add app icons to Home screen</string>
     <!-- Text description of the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=NONE] -->
     <string name="auto_add_shortcuts_description">For new apps</string>
@@ -342,7 +344,11 @@
     <!--- 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>
+    <string name="work_apps_paused_body">Work apps can\'t send you notifications, use your battery, or access your location</string>
+    <!-- content description for paused work apps list -->
+    <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
+
+
 
     <!-- A tip shown pointing at work toggle -->
     <string name="work_switch_tip">Pause work apps and notifications</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index bc6ab45..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,9 +50,12 @@
         <item name="folderFillColor">#CDFFFFFF</item>
         <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
         <item name="folderTextColor">#FF212121</item>
-        <item name="folderHintColor">#FF616161</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>
@@ -64,6 +69,8 @@
     <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">
@@ -78,7 +85,6 @@
         <item name="folderFillColor">#CDFFFFFF</item>
         <item name="folderIconBorderColor">#FF80868B</item>
         <item name="folderTextColor">?attr/workspaceTextColor</item>
-
     </style>
 
     <style name="LauncherTheme.Dark" parent="@style/LauncherTheme">
@@ -88,26 +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">#FFCCCCCC</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">
@@ -176,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>
@@ -225,7 +246,7 @@
     <style name="DropTargetButton" parent="DropTargetButtonBase" />
 
     <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
-    <style name="PrimaryMediumText" parent="@android:style/TextAppearance.DeviceDefault.Medium"/>
+    <style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
 
     <style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
 
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
index 7c7e73c..3fa9b0a 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -21,8 +21,11 @@
 LOCAL_MODULE := LauncherRoboTests
 LOCAL_MODULE_CLASS := JAVA_LIBRARIES
 
-LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+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 \
@@ -47,7 +50,7 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := RunLauncherRoboTests
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := system_current
 LOCAL_JAVA_LIBRARIES := LauncherRoboTests
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index 3d78689..a8e0cb3 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1 +1,19 @@
 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/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
index 74ef55e..b7ba106 100644
--- a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -16,24 +16,25 @@
 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.AppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.shadows.LShadowUserManager;
-import com.android.launcher3.util.LauncherRoboTestRunner;
+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(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public final class FolderNameProviderTest {
     private Context mContext;
     private WorkspaceItemInfo mItem1;
@@ -45,13 +46,13 @@
         mItem1 = new WorkspaceItemInfo(new AppInfo(
                 new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
                 "title1",
-                LShadowUserManager.newUserHandle(10),
+                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",
-                LShadowUserManager.newUserHandle(10),
+                UserHandle.of(10),
                 new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
         ));
     }
@@ -61,16 +62,16 @@
         ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
         list.add(mItem1);
         list.add(mItem2);
-        FolderNameInfo[] nameInfos =
-                new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+        FolderNameInfos nameInfos = new FolderNameInfos();
         new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
-        assertEquals("Work", nameInfos[0].getLabel());
+        assertEquals("Work", nameInfos.getLabels()[0]);
 
-        nameInfos[0] = new FolderNameInfo("candidate1", 0.9);
-        nameInfos[1] = new FolderNameInfo("candidate2", 0.8);
-        nameInfos[2] = new FolderNameInfo("candidate3", 0.7);
+        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[3].getLabel());
-
+        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 95a4146..c892618 100644
--- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
+++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -3,12 +3,11 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import com.android.launcher3.util.LauncherRoboTestRunner;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.Shadows;
 import org.robolectric.util.Scheduler;
@@ -21,7 +20,7 @@
 /**
  * Tests for {@link FileLog}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class FileLogTest {
 
     private File mTempDir;
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index b7f2243..8aa6f37 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -14,23 +14,23 @@
 import android.util.Pair;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.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 com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
@@ -41,7 +41,7 @@
 /**
  * Tests for {@link AddWorkspaceItemsTask}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class AddWorkspaceItemsTaskTest {
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
index 7072adf..90313ab 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -43,11 +43,11 @@
 import com.android.launcher3.shadows.LShadowBackupManager;
 import com.android.launcher3.shadows.LShadowUserManager;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.shadow.api.Shadow;
@@ -55,7 +55,7 @@
 /**
  * Tests to verify backup and restore flow.
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(LooperMode.Mode.PAUSED)
 public class BackupRestoreTest {
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index f128e24..5610b0e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -17,19 +17,19 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
@@ -40,7 +40,7 @@
 /**
  * Tests for {@link CacheDataUpdatedTask}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class CacheDataUpdatedTaskTest {
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index 1442c55..bbbe21e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -36,11 +36,11 @@
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.io.File;
@@ -48,7 +48,7 @@
 /**
  * Tests for {@link DbDowngradeHelper}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class DbDowngradeHelperTest {
 
     private static final String SCHEMA_FILE = "test_schema.json";
@@ -130,7 +130,7 @@
         }
         helper.close();
 
-        helper = new DatabaseHelper(mContext, 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, 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
index 7bc34cf..8f3a83e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -28,17 +28,17 @@
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
 
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfo;
 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 com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
@@ -46,7 +46,7 @@
 /**
  * Tests for layout parser for remote layout
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class DefaultLayoutProviderTest {
 
@@ -90,6 +90,22 @@
     }
 
     @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";
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
index f46b849..56ce215 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
@@ -23,17 +23,17 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 /**
  * Unit tests for {@link GridBackupTable}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class GridBackupTableTest {
 
     private static final int BACKUP_ITEM_COUNT = 12;
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index c426dc5..8e00dcb 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -20,11 +20,11 @@
 import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.HashSet;
@@ -33,7 +33,7 @@
 /**
  * Unit tests for {@link GridSizeMigrationTask}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class GridSizeMigrationTaskTest {
 
     private LauncherModelHelper mModelHelper;
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
index 8f58d8b..cca333c 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -39,17 +39,17 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.HashSet;
 
 /** Unit tests for {@link GridSizeMigrationTaskV2} */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class GridSizeMigrationTaskV2Test {
 
     private LauncherModelHelper mModelHelper;
@@ -59,6 +59,17 @@
     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();
@@ -67,6 +78,17 @@
 
         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(
@@ -78,20 +100,6 @@
 
     @Test
     public void testMigration() {
-        final String testPackage1 = "com.android.launcher3.validpackage1";
-        final String testPackage2 = "com.android.launcher3.validpackage2";
-        final String testPackage3 = "com.android.launcher3.validpackage3";
-        final String testPackage4 = "com.android.launcher3.validpackage4";
-        final String testPackage5 = "com.android.launcher3.validpackage5";
-        final String testPackage7 = "com.android.launcher3.validpackage7";
-
-        mValidPackages.add(testPackage1);
-        mValidPackages.add(testPackage2);
-        mValidPackages.add(testPackage3);
-        mValidPackages.add(testPackage4);
-        mValidPackages.add(testPackage5);
-        mValidPackages.add(testPackage7);
-
         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),
@@ -100,6 +108,10 @@
                 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,
@@ -108,21 +120,24 @@
         };
         mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
 
-        mIdp.numHotseatIcons = 3;
-        mIdp.numColumns = 3;
-        mIdp.numRows = 3;
+        mIdp.numHotseatIcons = 4;
+        mIdp.numColumns = 4;
+        mIdp.numRows = 4;
         GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
-                LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages, 5);
+                LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages,
+                srcHotseatItems.length);
         GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
-                LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages, 3);
+                LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages,
+                mIdp.numHotseatIcons);
         GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
-                destReader, 3, new Point(mIdp.numColumns, mIdp.numRows));
+                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(), 3);
+        assertEquals(c.getCount(), mIdp.numHotseatIcons);
         int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
         int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
         c.moveToNext();
@@ -134,13 +149,17 @@
         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(), 2);
+        assertEquals(c.getCount(), 6);
         intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
         int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX);
         int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY);
@@ -148,7 +167,23 @@
         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/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 6e41a4f..3a252dc 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -50,17 +50,17 @@
 import android.os.Process;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
+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.LauncherRoboTestRunner;
 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;
@@ -68,7 +68,7 @@
 /**
  * Tests for {@link LoaderCursor}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class LoaderCursorTest {
 
@@ -92,7 +92,8 @@
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
 
-        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp);
+        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp,
+                new UserManagerState());
         mLoaderCursor.allUsers.put(0, Process.myUserHandle());
     }
 
@@ -127,7 +128,7 @@
                         new Intent().setComponent(cn), false  /* allowMissingTarget */, true))
                 .get();
         assertNotNull(info);
-        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
     }
 
     @Test
@@ -141,7 +142,7 @@
                         new Intent().setComponent(cn), true  /* allowMissingTarget */, true))
                 .get();
         assertNotNull(info);
-        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
     }
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index c7979b2..87fe3c0 100644
--- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -26,20 +26,20 @@
 
 import android.os.Process;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.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.LauncherRoboTestRunner;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
@@ -55,7 +55,7 @@
 /**
  * Tests to verify multiple callbacks in Loader
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class ModelMultiCallbacksTest {
 
@@ -199,7 +199,7 @@
         }
 
         @Override
-        public void bindAllApplications(AppInfo[] apps) {
+        public void bindAllApplications(AppInfo[] apps, int flags) {
             mAppInfos = apps;
         }
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index bd71f01..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,16 @@
 
 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.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 com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
 
@@ -21,7 +21,7 @@
 /**
  * Tests for {@link PackageInstallStateChangedTask}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class PackageInstallStateChangedTaskTest {
 
diff --git a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
index 7612ae1..83bf7da 100644
--- a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
@@ -27,10 +27,9 @@
 
 import android.content.pm.ShortcutInfo;
 
-import com.android.launcher3.util.LauncherRoboTestRunner;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
@@ -40,7 +39,7 @@
 /**
  * Tests the sorting and filtering of shortcuts in {@link PopupPopulator}.
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class PopupPopulatorTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 7ef670c..ee73b82 100644
--- a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -23,16 +23,16 @@
 
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 /**
  * Tests for {@link RestoreDbTask}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class RestoreDbTaskTest {
 
     @Test
@@ -95,7 +95,7 @@
         private final long mProfileId;
 
         MyDatabaseHelper(long profileId) {
-            super(RuntimeEnvironment.application, 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/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/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
index 76cb747..6a6f0fb 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -30,7 +30,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
-import android.os.Process;
 import android.os.UserHandle;
 import android.util.ArraySet;
 
@@ -80,14 +79,15 @@
     protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
         ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
                 .resolveActivity(intent, 0);
-        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo);
+        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo, user);
     }
 
-    public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) {
+    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, Process.myUserHandle()));
+                ClassParameter.from(UserHandle.class, user));
     }
 
     @Implementation
@@ -104,7 +104,7 @@
                 .setPackage(packageName);
         return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
                 .stream()
-                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
                 .collect(Collectors.toList());
     }
 
@@ -130,7 +130,7 @@
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName);
         return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
                 .stream()
-                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
                 .collect(Collectors.toList());
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
index 576ddbd..edf8edb 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.shadows;
 
-import android.os.Parcel;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.SparseBooleanArray;
@@ -51,12 +50,4 @@
     public void setUserLocked(UserHandle userHandle, boolean enabled) {
         mLockedUsers.put(userHandle.hashCode(), enabled);
     }
-
-    // Create user handle from parcel since UserHandle.of() was only added in later APIs.
-    public static UserHandle newUserHandle(int uid) {
-        Parcel userParcel = Parcel.obtain();
-        userParcel.writeInt(uid);
-        userParcel.setDataPosition(0);
-        return new UserHandle(userParcel);
-    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/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
index 209bae0..d330d10 100644
--- a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
+++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
@@ -14,23 +14,20 @@
  * limitations under the License.
  */package com.android.launcher3.ui;
 
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
 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.app.Activity;
 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 android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -44,24 +41,21 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherLayoutBuilder.FolderBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.LauncherRoboTestRunner;
-import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.widget.WidgetsFullSheet;
 
 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.shadows.ShadowLooper;
-import org.robolectric.util.ReflectionHelpers;
 
 /**
  * Tests scroll behavior at various Launcher UI components
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 @LooperMode(Mode.PAUSED)
 public class LauncherUIScrollTest {
 
@@ -166,23 +160,7 @@
 
     private Launcher loadLauncher() throws Exception {
         mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder).loadModelSync();
-
-        Launcher launcher = Robolectric.buildActivity(Launcher.class).setup().get();
-        doLayout(launcher);
-        ViewOnDrawExecutor executor = ReflectionHelpers.getField(launcher, "mPendingExecutor");
-        if (executor != null) {
-            executor.runAllTasks();
-        }
-        return launcher;
-    }
-
-    private static void doLayout(Activity activity) {
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE
-                .get(RuntimeEnvironment.application).portraitProfile;
-        View view = activity.getWindow().getDecorView();
-        view.measure(makeMeasureSpec(dp.widthPx, EXACTLY), makeMeasureSpec(dp.heightPx, EXACTLY));
-        view.layout(0, 0, dp.widthPx, dp.heightPx);
-        ShadowLooper.idleMainLooper();
+        return buildAndBindLauncher();
     }
 
     private static MotionEvent createScrollEvent(int scroll) {
diff --git a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
index e453e31..2f3fc37 100644
--- a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
@@ -1,16 +1,17 @@
 package com.android.launcher3.util;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 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}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class GridOccupancyTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
index 5974ea5..c08e198 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
@@ -19,11 +19,12 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 
 /**
  * Robolectric unit tests for {@link IntArray}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class IntArrayTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
index aedf71e..7a8c00b 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
@@ -17,17 +17,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 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}
  */
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class IntSetTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/robolectric_tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
index d3659eb..4e21dce 100644
--- a/robolectric_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
index d593d84..0388087 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -36,9 +36,7 @@
 import android.os.Process;
 import android.provider.Settings;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
@@ -47,6 +45,8 @@
 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;
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
deleted file mode 100644
index 744b478b..0000000
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.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.launcher3.util;
-
-import static org.mockito.Mockito.mock;
-
-import com.android.launcher3.shadows.LShadowAppWidgetManager;
-import com.android.launcher3.shadows.LShadowBackupManager;
-import com.android.launcher3.shadows.LShadowBitmap;
-import com.android.launcher3.shadows.LShadowLauncherApps;
-import com.android.launcher3.shadows.LShadowTypeface;
-import com.android.launcher3.shadows.LShadowUserManager;
-import com.android.launcher3.shadows.LShadowWallpaperManager;
-import com.android.launcher3.shadows.ShadowDeviceFlag;
-import com.android.launcher3.shadows.ShadowLooperExecutor;
-import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
-import com.android.launcher3.shadows.ShadowOverrides;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-
-import org.junit.runners.model.InitializationError;
-import org.robolectric.DefaultTestLifecycle;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.TestLifecycle;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowLog;
-
-import java.lang.reflect.Method;
-
-import javax.annotation.Nonnull;
-
-/**
- * Test runner with Launcher specific configurations
- */
-public class LauncherRoboTestRunner extends RobolectricTestRunner {
-
-    private static final Class<?>[] SHADOWS = new Class<?>[] {
-            LShadowAppWidgetManager.class,
-            LShadowUserManager.class,
-            LShadowLauncherApps.class,
-            LShadowBitmap.class,
-            LShadowBackupManager.class,
-            LShadowTypeface.class,
-            LShadowWallpaperManager.class,
-            ShadowLooperExecutor.class,
-            ShadowMainThreadInitializedObject.class,
-            ShadowDeviceFlag.class,
-            ShadowOverrides.class
-    };
-
-    public LauncherRoboTestRunner(Class<?> testClass) throws InitializationError {
-        super(testClass);
-    }
-
-    @Override
-    protected Config buildGlobalConfig() {
-        return new Config.Builder().setShadows(SHADOWS).build();
-    }
-
-    @Nonnull
-    @Override
-    protected Class<? extends TestLifecycle> getTestLifecycleClass() {
-        return LauncherTestLifecycle.class;
-    }
-
-    public static class LauncherTestLifecycle extends DefaultTestLifecycle {
-
-        @Override
-        public void beforeTest(Method method) {
-            super.beforeTest(method);
-            ShadowLog.stream = System.out;
-
-            // Disable plugins
-            PluginManagerWrapper.INSTANCE.initializeForTesting(mock(PluginManagerWrapper.class));
-
-            // Initialize mock wallpaper manager
-            LShadowWallpaperManager.initializeMock();
-        }
-
-        @Override
-        public void afterTest(Method method) {
-            super.afterTest(method);
-
-            ShadowLog.stream = null;
-            ShadowMainThreadInitializedObject.resetInitializedObjects();
-            ShadowOverrides.clearProvider();
-        }
-    }
-}
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/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index daae818..84c65b1 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -34,15 +34,15 @@
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.LauncherRoboTestRunner;
+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;
@@ -50,7 +50,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 
-@RunWith(LauncherRoboTestRunner.class)
+@RunWith(RobolectricTestRunner.class)
 public class WidgetsListAdapterTest {
 
     @Mock private LayoutInflater mMockLayoutInflater;
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index bed8278..cd27a2d 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -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 |
@@ -170,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);
@@ -180,6 +185,11 @@
         return null;
     }
 
+    /** Returns the View that Accessibility services should focus on first. */
+    protected View getAccessibilityInitialFocusView() {
+        return this;
+    }
+
     /**
      * Returns a view matching FloatingViewType
      */
@@ -253,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 73d8a88..6f7b684 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -350,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,
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 71b7206..a55c90d 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -17,6 +17,7 @@
 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;
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 814b728..310c306 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -37,11 +37,7 @@
 
 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.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
@@ -54,7 +50,7 @@
 /**
  * Launcher BaseActivity
  */
-public abstract class BaseActivity extends Activity implements LogStateProvider, ActivityContext {
+public abstract class BaseActivity extends Activity implements ActivityContext {
 
     private static final String TAG = "BaseActivity";
 
@@ -110,9 +106,14 @@
     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 << 5;
+    public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 6;
 
     @Retention(SOURCE)
     @IntDef(
@@ -143,13 +144,11 @@
         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;
     }
@@ -182,6 +181,7 @@
     @Override
     protected void onResume() {
         addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
+        removeActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
         super.onResume();
     }
 
@@ -332,7 +332,6 @@
             return;
         }
         try {
-            TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: shortcut", packageName);
             getSystemService(LauncherApps.class).startShortcut(packageName, id, sourceBounds,
                     startActivityOptions, user);
         } catch (SecurityException | IllegalStateException e) {
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 6fa3c28..9cb8cf2 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.APP_LAUNCH_TAP;
+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;
@@ -24,6 +24,8 @@
 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;
@@ -31,16 +33,21 @@
 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.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
+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;
@@ -49,6 +56,7 @@
 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
@@ -171,7 +179,6 @@
                 startShortcutIntentSafely(intent, optsBundle, item, sourceContainer);
             } else if (user == null || user.equals(Process.myUserHandle())) {
                 // Could be launching some bookkeeping activity
-                TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: activity", intent);
                 startActivity(intent, optsBundle);
                 AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(),
                         Process.myUserHandle(), sourceContainer);
@@ -182,7 +189,10 @@
                         sourceContainer);
             }
             getUserEventDispatcher().logAppLaunch(v, intent, user);
-            getStatsLogManager().log(APP_LAUNCH_TAP, item.buildProto(null, null));
+            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();
@@ -191,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 {
@@ -269,4 +284,20 @@
     }
 
     protected abstract void reapplyUi();
+
+    protected WindowBounds getMultiWindowDisplaySize() {
+        if (Utilities.ATLEAST_R) {
+            WindowMetrics wm = getWindowManager().getCurrentWindowMetrics();
+
+            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 9e8441a..8eceec0 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -16,17 +16,21 @@
 
 package com.android.launcher3;
 
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -179,12 +183,24 @@
     public void onScrollStateChanged(int state) {
         super.onScrollStateChanged(state);
 
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onScrollStateChanged: " + state);
-        }
-
         if (state == SCROLL_STATE_IDLE) {
             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 921e8ac..60b6da6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -29,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;
@@ -53,7 +54,13 @@
 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;
 
@@ -65,7 +72,7 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
-        IconLabelDotView, DraggableView {
+        IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
@@ -73,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") {
@@ -667,23 +678,79 @@
         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 getVisualDragBounds(Rect bounds) {
+    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 void prepareDrawDragView() {
+    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 34d7067..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;
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 4259196..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;
@@ -54,18 +55,17 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
 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.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.widget.LauncherAppWidgetHostView;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -98,6 +98,7 @@
     // 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;
@@ -135,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;
 
@@ -181,7 +182,7 @@
     private static final Paint sPaint = new Paint();
 
     // Related to accessible drag and drop
-    private boolean mUseTouchHelper = false;
+    DragAndDropAccessibilityDelegate mTouchHelper;
 
     public CellLayout(Context context) {
         this(context, null);
@@ -289,17 +290,15 @@
         addView(mShortcutsAndWidgets);
     }
 
-
     /**
      * Sets or clears a delegate used for accessible drag and drop
      */
     public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) {
         setOnClickListener(delegate);
-        setOnHoverListener(delegate);
         ViewCompat.setAccessibilityDelegate(this, delegate);
 
-        mUseTouchHelper = delegate != null;
-        int accessibilityFlag = mUseTouchHelper
+        mTouchHelper = delegate;
+        int accessibilityFlag = mTouchHelper != null
                 ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO;
         setImportantForAccessibility(accessibilityFlag);
         getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
@@ -312,9 +311,18 @@
     }
 
     @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // Always attempt to dispatch hover events to accessibility first.
+        if (mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)) {
+            return true;
+        }
+        return super.dispatchHoverEvent(event);
+    }
+
+    @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;
@@ -851,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)) {
@@ -861,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;
@@ -877,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;
             }
@@ -898,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() {
@@ -911,6 +933,7 @@
                     // place just yet.
                     if (!cancelled) {
                         lp.isLockedToGrid = true;
+                        item.setReorderPreviewOffset(0, 0);
                         child.requestLayout();
                     }
                     if (mReorderAnimators.containsKey(lp)) {
@@ -959,8 +982,13 @@
                 height = r.height();
             }
 
-            if (v != null && v.getViewType() == DraggableView.DRAGGABLE_ICON) {
-                left +=  ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+            // 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;
@@ -1862,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();
             }
         }
@@ -1887,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;
@@ -1907,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];
@@ -1920,63 +1949,60 @@
 
             this.child = child;
             this.mode = mode;
+            finalDeltaX = 0;
+            finalDeltaY = 0;
 
-            // TODO issue!
-            setInitialAnimationValues(false);
-            finalScale = (mChildScale - (CHILD_DIVIDEND / child.getWidth())) * initScale;
-            finalDeltaX = initDeltaX;
-            finalDeltaY = initDeltaY;
+            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;
 
@@ -1993,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;
                 }
             });
@@ -2006,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() {
@@ -2019,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();
     }
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index ef353f9..ff405ec 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -113,7 +113,10 @@
     }
 
     private void triggerLongPress() {
-        if ((mView.getParent() != null) && mView.hasWindowFocus() && !mHasPerformedLongPress) {
+        if ((mView.getParent() != null)
+                && mView.hasWindowFocus()
+                && (!mView.isPressed() || mListener == null)
+                && !mHasPerformedLongPress) {
             boolean handled;
             if (mListener != null) {
                 handled = mListener.onLongClick(mView);
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 6f0ebd2..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
@@ -116,6 +126,9 @@
             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
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 4e1e586..e3cd0bd 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;
@@ -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;
@@ -212,13 +217,14 @@
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
         // Add a bit of space between nav bar and hotseat in vertical bar layout.
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, dm) + (isVerticalBarLayout()
+        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) {
@@ -232,20 +238,7 @@
             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 + 2 * 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, null, 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,11 +365,19 @@
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
-        allAppsIconSizePx = iconSizePx;
-        allAppsIconTextSizePx = iconTextSizePx;
-        allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
-        allAppsCellHeightPx = getCellSize().y;
-        allAppsCellWidthPx = allAppsIconSizePx + 2 * allAppsIconDrawablePaddingPx;
+        // All apps
+        if (allAppsHasDifferentNumColumns()) {
+            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
+            allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
+            allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
+        } else {
+            allAppsIconSizePx = iconSizePx;
+            allAppsIconTextSizePx = iconTextSizePx;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
+            allAppsCellHeightPx = getCellSize().y;
+        }
+        allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
 
         if (isVerticalBarLayout()) {
             // Always hide the Workspace text with vertical bar layout.
@@ -391,12 +408,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 +432,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);
 
@@ -521,7 +538,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 +546,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;
@@ -618,10 +636,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 +656,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 ef02e87..b27abc4 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -25,6 +25,10 @@
 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.
@@ -74,9 +78,16 @@
          * 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()) {
-                folderNameProvider = FolderNameProvider.newInstance(context);
+                Executors.MODEL_EXECUTOR.post(() -> {
+                    folderNameProvider = FolderNameProvider.newInstance(context);
+                });
             }
         }
 
@@ -102,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/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index a78159f..d3b86de 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -35,6 +35,8 @@
 
 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 {
@@ -43,7 +45,6 @@
 
     private static final float DISABLED_DESATURATION = 1f;
     private static final float DISABLED_BRIGHTNESS = 0.5f;
-    private static final float DISABLED_ALPHA = 0.54f;
 
     public static final int CLICK_FEEDBACK_DURATION = 200;
 
@@ -55,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
@@ -252,7 +254,7 @@
             mat[4] = brightnessI;
             mat[9] = brightnessI;
             mat[14] = brightnessI;
-            mat[18] = DISABLED_ALPHA;
+            mat[18] = mDisabledAlpha;
             tempFilterMatrix.preConcat(tempBrightnessMatrix);
             sDisabledFColorFilter = new ColorMatrixColorFilter(tempFilterMatrix);
         }
@@ -318,12 +320,15 @@
      * Creates a drawable for the provided BitmapInfo
      */
     public static FastBitmapDrawable newIcon(Context context, BitmapInfo info) {
+        final FastBitmapDrawable drawable;
         if (info instanceof Factory) {
-            return ((Factory) info).newDrawable();
+            drawable = ((Factory) info).newDrawable();
         } else if (info.isLowRes()) {
-            return new PlaceHolderIconDrawable(info, context);
+            drawable = new PlaceHolderIconDrawable(info, context);
         } else {
-            return new FastBitmapDrawable(info);
+            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 f07040d..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;
 
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
deleted file mode 100644
index b75a5e7..0000000
--- a/src/com/android/launcher3/FolderInfo.java
+++ /dev/null
@@ -1,156 +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.Intent;
-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 static final int FLAG_MANUAL_FOLDER_NAME = 0x00000008;
-
-    public static final String EXTRA_FOLDER_SUGGESTIONS = "suggest";
-
-    public int options;
-
-    public Intent 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 super.dumpProperties()
-                + " manuallyTypedTitle=" + hasOption(FLAG_MANUAL_FOLDER_NAME);
-    }
-}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 78bd2ff..51f3819 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -28,6 +28,7 @@
 import android.widget.FrameLayout;
 
 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;
 
@@ -37,6 +38,8 @@
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mHasVerticalHotseat;
+    private Workspace mWorkspace;
+    private boolean mSendTouchToWorkspace;
 
     public Hotseat(Context context) {
         this(context, null);
@@ -88,7 +91,6 @@
     public void setInsets(Rect insets) {
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
         DeviceProfile grid = mActivity.getDeviceProfile();
-        insets = grid.getInsets();
 
         if (grid.isVerticalBarLayout()) {
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
@@ -111,8 +113,35 @@
         InsettableFrameLayout.dispatchInsets(this, insets);
     }
 
+    public void setWorkspace(Workspace w) {
+        mWorkspace = w;
+    }
+
+    @Override
+    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/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 a5142d8..62c9b4d 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -46,6 +46,10 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
+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;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 7414a88..e39e89c 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -17,8 +17,8 @@
 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.settings.SettingsActivity.GRID_OPTIONS_PREFERENCE_KEY;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
@@ -47,7 +47,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.DefaultDisplay.Info;
@@ -61,7 +60,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 
 public class InvariantDeviceProfile {
 
@@ -70,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;
@@ -107,6 +108,8 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public float iconTextSize;
+    public float allAppsIconSize;
+    public float allAppsIconTextSize;
 
     private SparseArray<TypedValue> mExtraAttrs;
 
@@ -145,10 +148,13 @@
         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;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
@@ -157,13 +163,16 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
-        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
-            throw new IllegalArgumentException(
-                    "PreviewContext is passed into this IDP constructor");
-        }
-
         String gridName = getCurrentGridName(context);
-        initGrid(context, gridName);
+        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);
@@ -183,13 +192,31 @@
      * This constructor should NOT have any monitors by design.
      */
     public InvariantDeviceProfile(Context context, Display display) {
-        initGrid(context, null, new Info(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.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
+        return Utilities.isGridOptionsEnabled(context)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
     }
 
     /**
@@ -204,18 +231,47 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        return initGrid(context, gridName, DefaultDisplay.INSTANCE.get(context).getInfo());
+        DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
+        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
+
+        DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
+        initGrid(context, displayInfo, displayOption);
+        return displayOption.grid.name;
     }
 
-    private String initGrid(Context context, String gridName, DefaultDisplay.Info displayInfo) {
-        Point smallestSize = new Point(displayInfo.smallestSize);
-        Point largestSize = new Point(displayInfo.largestSize);
+    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;
 
-        // 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);
+        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
@@ -223,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
@@ -294,38 +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;
-        dbFile = closestProfile.dbFile;
-        defaultLayoutId = closestProfile.defaultLayoutId;
-        demoModeLayoutId = closestProfile.demoModeLayoutId;
-        numFolderRows = closestProfile.numFolderRows;
-        numFolderColumns = closestProfile.numFolderColumns;
-        numAllAppsColumns = numColumns;
-
-        mExtraAttrs = closestProfile.extraAttrs;
-
-        iconSize = displayOption.iconSize;
-        iconShapePath = getIconShapePath(context);
-        landscapeIconSize = displayOption.landscapeIconSize;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
-        iconTextSize = displayOption.iconTextSize;
-        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
-
-        // If the partner customization apk contains any grid overrides, apply them
-        // Supported overrides: numRows, numColumns, iconSize
-        applyPartnerDeviceProfileOverrides(context, metrics);
-    }
-
-
     @Nullable
     public TypedValue getAttrValue(int attr) {
         return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
@@ -368,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;
@@ -403,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();
@@ -436,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) {
@@ -492,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);
@@ -573,6 +581,8 @@
         private final int numHotseatIcons;
 
         private final String dbFile;
+        private final int numAllAppsColumns;
+
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
@@ -596,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();
 
@@ -607,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;
@@ -615,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;
@@ -622,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(
@@ -633,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;
@@ -647,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;
         }
     }
@@ -672,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 6f5b941..0000000
--- a/src/com/android/launcher3/ItemInfo.java
+++ /dev/null
@@ -1,303 +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 static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-
-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.logger.LauncherAtom;
-import com.android.launcher3.util.ContentWriter;
-
-
-
-/**
- * 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 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)
-                + " 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 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
-                || container == LauncherSettings.Favorites.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(Intent intent, FolderInfo fInfo) {
-
-        LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
-        itemBuilder.setIsWork(user != Process.myUserHandle());
-        ComponentName cn = getTargetComponent();
-        if (cn == null) return itemBuilder.build();
-        switch (itemType) {
-            case ITEM_TYPE_APPLICATION:
-                itemBuilder.setApplication(LauncherAtom.Application.newBuilder()
-                        .setComponentName(cn.flattenToShortString())
-                        .setPackageName(cn.getPackageName()));
-                break;
-            case ITEM_TYPE_DEEP_SHORTCUT:
-            case ITEM_TYPE_SHORTCUT:
-                itemBuilder.setShortcut(LauncherAtom.Shortcut.newBuilder()
-                        .setShortcutName(cn.flattenToShortString()));
-                break;
-            case ITEM_TYPE_APPWIDGET:
-                setItemBuilder(itemBuilder);
-                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:
-                    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.setFolder(folderBuilder);
-        } else {
-            switch (container) {
-                case CONTAINER_HOTSEAT:
-                    itemBuilder.setHotseat(LauncherAtom.HotseatContainer.newBuilder()
-                            .setIndex(screenId));
-                    break;
-                case CONTAINER_DESKTOP:
-                    itemBuilder.setWorkspace(LauncherAtom.WorkspaceContainer.newBuilder()
-                            .setGridX(cellX)
-                            .setGridY(cellY)
-                            .setPageIndex(screenId));
-                    break;
-            }
-        }
-        return itemBuilder.build();
-    }
-}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 043ea2f..0970dae 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -18,21 +18,34 @@
 
 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.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 android.animation.Animator;
@@ -55,11 +68,9 @@
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Point;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.Handler;
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.StrictMode;
@@ -67,7 +78,6 @@
 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;
@@ -80,19 +90,19 @@
 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;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
@@ -104,12 +114,19 @@
 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.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;
@@ -117,6 +134,10 @@
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.qsb.QsbContainerView;
+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;
@@ -133,6 +154,7 @@
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
@@ -179,7 +201,7 @@
 /**
  * Default launcher application.
  */
-public class Launcher extends BaseDraggingActivity implements LauncherExterns,
+public class Launcher extends StatefulActivity<LauncherState> implements LauncherExterns,
         Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin> {
     public static final String TAG = "Launcher";
 
@@ -226,7 +248,7 @@
     public static final String ON_RESUME_EVT = "Launcher.onResume";
     public static final String ON_NEW_INTENT_EVT = "Launcher.onNewIntent";
 
-    private LauncherStateManager mStateManager;
+    private StateManager<LauncherState> mStateManager;
 
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
 
@@ -243,7 +265,6 @@
 
     @Thunk
     Workspace mWorkspace;
-    private View mLauncherView;
     @Thunk
     DragLayer mDragLayer;
     private DragController mDragController;
@@ -294,6 +315,7 @@
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
     private SharedPreferences mSharedPrefs;
+    private OnboardingPrefs mOnboardingPrefs;
 
     // Activity result which needs to be processed after workspace has loaded.
     private ActivityResultInfo mPendingActivityResult;
@@ -309,10 +331,6 @@
 
     private RotationHelper mRotationHelper;
 
-    final Handler mHandler = new Handler();
-    private final Runnable mHandleDeferredResume = this::handleDeferredResume;
-    private boolean mDeferredResumePending;
-
     private float mCurrentAssistantVisibility = 0f;
 
     protected LauncherOverlayManager mOverlayManager;
@@ -349,6 +367,7 @@
         LauncherAppState app = LauncherAppState.getInstance(this);
         mOldConfig = new Configuration(getResources().getConfiguration());
         mModel = app.getModel();
+
         mRotationHelper = new RotationHelper(this);
         InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
         initDeviceProfile(idp);
@@ -359,15 +378,16 @@
 
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
-        mStateManager = new LauncherStateManager(this);
+        mStateManager = new StateManager<>(this, NORMAL);
+
+        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::updateNotificationDots);
 
@@ -404,7 +424,7 @@
         // For handling default keys
         setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
 
-        setContentView(mLauncherView);
+        setContentView(getRootView());
         getRootView().dispatchInsets();
 
         // Listen for broadcasts
@@ -422,10 +442,7 @@
 
         mRotationHelper.initialize();
 
-        mStateManager.addStateListener(new LauncherStateManager.StateListener() {
-            @Override
-            public void onStateTransitionStart(LauncherState toState) {
-            }
+        mStateManager.addStateListener(new StateListener<LauncherState>() {
 
             @Override
             public void onStateTransitionComplete(LauncherState finalState) {
@@ -452,6 +469,14 @@
         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));
@@ -499,16 +524,6 @@
     }
 
     @Override
-    public void reapplyUi() {
-        reapplyUi(true /* cancelCurrentAnimation */);
-    }
-
-    public void reapplyUi(boolean cancelCurrentAnimation) {
-        getRootView().dispatchInsets();
-        getStateManager().reapplyState(cancelCurrentAnimation);
-    }
-
-    @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
         onIdpChanged(idp);
     }
@@ -543,12 +558,8 @@
         // 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());
         }
 
         onDeviceProfileInitiated();
@@ -563,13 +574,9 @@
         return mFocusHandler;
     }
 
-    public LauncherStateManager getStateManager() {
-        return mStateManager;
-    }
-
     @Override
-    public <T extends View> T findViewById(int id) {
-        return mLauncherView.findViewById(id);
+    public StateManager<LauncherState> getStateManager() {
+        return mStateManager;
     }
 
     private LauncherCallbacks mLauncherCallbacks;
@@ -870,11 +877,7 @@
 
     @Override
     protected void onStop() {
-        final boolean wasActive = isUserActive();
-        final LauncherState origState = getStateManager().getState();
-        final int origDragLayerChildCount = mDragLayer.getChildCount();
         super.onStop();
-
         if (mDeferOverlayCallbacks) {
             checkIfOverlayStillDeferred();
         } else {
@@ -882,30 +885,8 @@
         }
 
         logStopAndResume(Action.Command.STOP);
-
         mAppWidgetHost.setListenIfResumed(false);
-
         NotificationListener.removeNotificationsChangedListener();
-        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.
-            mDragLayer.post(() -> {
-                if ((!getStateManager().isInStableState(origState)
-                        // The drag layer may be animating (e.g. dismissing QSB).
-                        || mDragLayer.getAlpha() < 1
-                        // Maybe an ArrowPopup is closed.
-                        || mDragLayer.getChildCount() != origDragLayerChildCount)) {
-                    onUiChangedWhileSleeping();
-                }
-            });
-        }
-
-        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Activity.onStop");
     }
 
     @Override
@@ -919,47 +900,57 @@
 
         mAppWidgetHost.setListenIfResumed(true);
         TraceHelper.INSTANCE.endSection(traceToken);
-        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Activity.onStart");
     }
 
-    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();
 
-            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);
-
-            onDeferredResumed();
-            addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
-
-            mDeferredResumePending = false;
-        } else {
-            mDeferredResumePending = true;
-        }
+        DiscoveryBounce.showForHomeIfNeeded(this);
     }
 
-    protected void onDeferredResumed() { }
 
     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() {
@@ -971,7 +962,8 @@
         if (!mDeferOverlayCallbacks) {
             return;
         }
-        if (isStarted() && (!hasBeenResumed() || mStateManager.getState().disableInteraction)) {
+        if (isStarted() && (!hasBeenResumed()
+                || mStateManager.getState().hasFlag(FLAG_NON_INTERACTIVE))) {
             return;
         }
         mDeferOverlayCallbacks = false;
@@ -998,21 +990,51 @@
         return mOverlayManager;
     }
 
+    @Override
     public void onStateSetStart(LauncherState state) {
-        if (mDeferredResumePending) {
-            handleDeferredResume();
-        }
+        super.onStateSetStart(state);
         if (mDeferOverlayCallbacks) {
             scheduleDeferredCheck();
         }
         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
@@ -1021,9 +1043,6 @@
                 TraceHelper.FLAG_UI_EVENT);
         super.onResume();
 
-        mHandler.removeCallbacks(mHandleDeferredResume);
-        Utilities.postAsyncCallback(mHandler, mHandleDeferredResume);
-
         if (!mOnResumeCallbacks.isEmpty()) {
             final ArrayList<OnResumeCallback> resumeCallbacks = new ArrayList<>(mOnResumeCallbacks);
             mOnResumeCallbacks.clear();
@@ -1066,10 +1085,6 @@
         }
     }
 
-    public boolean isInState(LauncherState state) {
-        return mStateManager.getState() == state;
-    }
-
     /**
      * Restores the previous state, if it exists.
      *
@@ -1083,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 */);
         }
 
@@ -1113,10 +1128,7 @@
         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);
@@ -1308,8 +1320,6 @@
         }
     };
 
-    protected void onUiChangedWhileSleeping() { }
-
     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
         mWorkspace.updateNotificationDots(updatedDots);
         mAppsView.getAppsStore().updateNotificationDots(updatedDots);
@@ -1333,11 +1343,6 @@
     }
 
     @Override
-    public LauncherRootView getRootView() {
-        return (LauncherRootView) mLauncherView;
-    }
-
-    @Override
     public DragLayer getDragLayer() {
         return mDragLayer;
     }
@@ -1443,6 +1448,8 @@
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+        } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
+            getStateManager().goToState(ALL_APPS, alreadyOnHome);
         }
 
         TraceHelper.INSTANCE.endSection(traceToken);
@@ -1526,6 +1533,7 @@
         mOverlayManager.onActivityDestroyed(this);
         mAppTransitionManager.unregisterRemoteAnimations();
         mUserChangedCallbackCloseable.close();
+        mAllAppsController.onActivityDestroyed();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1653,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);
         }
@@ -1851,16 +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 boolean startActivitySafely(View v, Intent intent, ItemInfo item,
             @Nullable String sourceContainer) {
         if (!hasBeenResumed()) {
@@ -2175,6 +2174,9 @@
         workspace.requestLayout();
     }
 
+    @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
     /**
      * Add the views for a widget to the workspace.
      */
@@ -2446,8 +2448,9 @@
      *
      * 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);
     }
 
     /**
@@ -2557,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);
@@ -2666,7 +2670,7 @@
         return super.onKeyUp(keyCode, event);
     }
 
-    protected StateHandler[] createStateHandlers() {
+    protected StateHandler<LauncherState>[] createStateHandlers() {
         return new StateHandler[] { getAllAppsController(), getWorkspace() };
     }
 
@@ -2674,10 +2678,6 @@
         return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
     }
 
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
 
     public void onDragLayerHierarchyChanged() { }
@@ -2700,6 +2700,14 @@
         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);
     }
@@ -2711,6 +2719,7 @@
         return (T) activityContext;
     }
 
+
     /**
      * Callback for listening for onResume
      */
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 2f38037..53e5274 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -33,6 +33,7 @@
 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;
@@ -57,6 +58,7 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
+    private final PredictionModel mPredictionModel;
 
     private SecureSettingsObserver mNotificationDotsObserver;
     private InstallSessionTracker mInstallSessionTracker;
@@ -127,6 +129,7 @@
         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) {
@@ -182,6 +185,10 @@
         return mModel;
     }
 
+    public PredictionModel getPredictionModel() {
+        return mPredictionModel;
+    }
+
     public WidgetPreviewLoader getWidgetCache() {
         return mWidgetCache;
     }
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index 9148c2f..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,17 +55,6 @@
     }
 
     /**
-     * Number of animations which run on state properties.
-     */
-    public int getStateElementAnimationsCount() {
-        return 0;
-    }
-
-    public Animator createStateElementAnimation(int index, float... values) {
-        throw new RuntimeException("Unknown gesture animation " + index);
-    }
-
-    /**
      * Registers remote animations for certain system transitions.
      */
     public void registerRemoteAnimations() {
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index 9921f76..7ea6851 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -30,6 +30,8 @@
 import android.widget.Toast;
 
 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;
@@ -298,6 +300,7 @@
         }
 
         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 56cce78..618b5de 100644
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
@@ -13,6 +13,7 @@
 
 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
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e61b7a8..f434c91 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -51,6 +51,9 @@
 import com.android.launcher3.model.PackageUpdatedTask;
 import com.android.launcher3.model.ShortcutsChangedTask;
 import com.android.launcher3.model.UserLockStateChangedTask;
+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;
@@ -114,7 +117,7 @@
         @Override
         public void run() {
             if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
-                    != mBgDataModel.hasShortcutHostPermission) {
+                    != mBgAllAppsList.hasShortcutHostPermission()) {
                 forceReload();
             }
         }
@@ -235,7 +238,8 @@
                 // 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_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 8d20bd6..e8b5568 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.backup.BackupManager;
@@ -48,7 +47,6 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -85,6 +83,7 @@
 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 {
@@ -92,8 +91,7 @@
     private static final boolean LOGD = false;
 
     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
-    private static final String TOKEN_RESTORE_BACKUP_TABLE = "restore_backup_table";
-    private static final long RESTORE_BACKUP_TABLE_DELAY = 60000;
+    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.
@@ -107,6 +105,8 @@
 
     protected DatabaseHelper mOpenHelper;
 
+    private long mLastRestoreTimestamp = 0L;
+
     /**
      * $ adb shell dumpsys activity provider com.android.launcher3
      */
@@ -146,7 +146,8 @@
      */
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = DatabaseHelper.createDatabaseHelper(getContext());
+            mOpenHelper = DatabaseHelper.createDatabaseHelper(
+                    getContext(), false /* forMigration */);
 
             if (RestoreDbTask.isPending(getContext())) {
                 if (!RestoreDbTask.performRestore(getContext(), mOpenHelper,
@@ -406,20 +407,22 @@
                 return result;
             }
             case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
-                // TODO(pinyaoting): Update the behavior here.
-                if (!MULTI_DB_GRID_MIRATION_ALGO.get()) {
-                    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 Handler handler = MODEL_EXECUTOR.getHandler();
-                handler.removeCallbacksAndMessages(TOKEN_RESTORE_BACKUP_TABLE);
-                handler.postDelayed(() -> RestoreDbTask.restoreIfPossible(
-                        getContext(), mOpenHelper, new BackupManager(getContext())),
-                        TOKEN_RESTORE_BACKUP_TABLE, RESTORE_BACKUP_TABLE_DELAY);
+                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: {
@@ -430,7 +433,8 @@
                                     InvariantDeviceProfile.INSTANCE.get(getContext()).dbFile,
                                     Favorites.TMP_TABLE,
                                     () -> mOpenHelper,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext())));
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), true /* forMigration */)));
                     return result;
                 }
             }
@@ -441,7 +445,8 @@
                             prepForMigration(
                                     arg /* dbFile */,
                                     Favorites.PREVIEW_TABLE_NAME,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext(), arg),
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), arg, true /* forMigration */),
                                     () -> mOpenHelper));
                     return result;
                 }
@@ -451,11 +456,7 @@
     }
 
     private void onAddOrDeleteOp(SQLiteDatabase db) {
-        if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
-            // TODO(pingyaoting): Implement the behavior here.
-        } else {
-            mOpenHelper.onAddOrDeleteOp(db);
-        }
+        mOpenHelper.onAddOrDeleteOp(db);
     }
 
     /**
@@ -609,20 +610,23 @@
     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;
 
-        static DatabaseHelper createDatabaseHelper(Context context) {
-            return createDatabaseHelper(context, null);
+        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
+            return createDatabaseHelper(context, null, forMigration);
         }
 
-        static DatabaseHelper createDatabaseHelper(Context context, String dbName) {
+        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);
+            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.
@@ -635,6 +639,8 @@
                 databaseHelper.mBackupTableExists = tableExists(
                         databaseHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
             }
+            databaseHelper.mHotseatRestoreTableExists = tableExists(
+                    databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
 
             databaseHelper.initIds();
             return databaseHelper;
@@ -643,9 +649,10 @@
         /**
          * Constructor used in tests and for restore.
          */
-        public DatabaseHelper(Context context, String dbName) {
+        public DatabaseHelper(Context context, String dbName, boolean forMigration) {
             super(context, dbName, SCHEMA_VERSION);
             mContext = context;
+            mForMigration = forMigration;
         }
 
         protected void initIds() {
@@ -670,14 +677,20 @@
 
             // Fresh and clean launcher DB.
             mMaxItemId = initializeMaxItemId(db);
-            onEmptyDbCreated();
+            if (!mForMigration) {
+                onEmptyDbCreated();
+            }
         }
 
         protected void onAddOrDeleteOp(SQLiteDatabase db) {
-            if (!MULTI_DB_GRID_MIRATION_ALGO.get() && mBackupTableExists) {
+            if (mBackupTableExists) {
                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
                 mBackupTableExists = false;
             }
+            if (mHotseatRestoreTableExists) {
+                dropTable(db, Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+                mHotseatRestoreTableExists = false;
+            }
         }
 
         /**
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index b4fbbc3..51504ce 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -1,21 +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.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;
 
@@ -23,18 +19,12 @@
 
     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;
@@ -43,64 +33,17 @@
 
     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.getDeviceProfile().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 */);
         }
     }
 
@@ -109,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
@@ -127,28 +65,10 @@
     }
 
     public void dispatchInsets() {
-        mLauncher.getDeviceProfile().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;
     }
@@ -184,7 +104,7 @@
 
     @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;
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 5262b18..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.
  */
@@ -98,6 +100,11 @@
         public static final String BACKUP_TABLE_NAME = "favorites_bakup";
 
         /**
+         * Backup table created when user hotseat is moved to workspace for hybrid hotseat
+         */
+        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";
@@ -150,17 +157,28 @@
         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";
@@ -233,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>
@@ -322,6 +346,8 @@
 
         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";
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 504666a..c78df62 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -15,52 +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.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-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.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
 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 android.content.Context;
-import android.view.View;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.allapps.AllAppsContainerView;
+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.states.StateAnimationConfig;
+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 abstract class LauncherState {
-
+public abstract class LauncherState implements BaseState<LauncherState> {
 
     /**
      * Set of elements indicating various workspace elements which change visibility across states
@@ -79,16 +62,27 @@
     public static final int APPS_VIEW_ITEM_MASK =
             HOTSEAT_SEARCH_BOX | ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT;
 
-    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;
+    // 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) {
@@ -98,7 +92,7 @@
                 }
             };
 
-    private static final LauncherState[] sAllStates = new LauncherState[8];
+    private static final LauncherState[] sAllStates = new LauncherState[9];
 
     /**
      * TODO: Create a separate class for NORMAL state.
@@ -108,7 +102,7 @@
             FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_HIDE_BACK_BUTTON |
                     FLAG_HAS_SYS_UI_SCRIM) {
         @Override
-        public int getTransitionDuration(Launcher launcher) {
+        public int getTransitionDuration(Context context) {
             // Arbitrary duration, when going to NORMAL we use the state we're coming from instead.
             return 0;
         }
@@ -125,6 +119,8 @@
     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 =
@@ -138,89 +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;
-
-    /**
-     * 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;
-
-    public final boolean hasSysUiScrim;
+    private final int mFlags;
 
     public LauncherState(int id, int containerType, int flags) {
         this.containerType = containerType;
-
-        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);
     }
 
-    /**
-     * @return How long the animation to this state should take (or from this state to NORMAL).
-     * @param launcher
-     */
-    public abstract int getTransitionDuration(Launcher launcher);
-
     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) {
@@ -228,24 +169,24 @@
         return getWorkspaceScaleAndTranslation(launcher);
     }
 
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return launcher.getOverviewScaleAndTranslationForNormalState();
+    /**
+     * 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(1, 0, 0);
+        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;
@@ -271,12 +212,36 @@
     }
 
     /**
+     * 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 float getDepth(Context context) {
+    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;
     }
 
@@ -297,82 +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,
-            StateAnimationConfig config) {
-        if (this == NORMAL && fromState == OVERVIEW) {
-            config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
-            config.setInterpolator(ANIM_WORKSPACE_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 = 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);
-                if (ENABLE_OVERVIEW_ACTIONS.get()) {
-                    AllAppsContainerView qsbContainer = launcher.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 (this == NORMAL && fromState == OVERVIEW_PEEK) {
-            // Keep fully visible until the very end (when overview is offscreen) to make invisible.
-            config.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/PagedView.java b/src/com/android/launcher3/PagedView.java
index 5e47e2f..e29faac 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -38,7 +38,6 @@
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.Surface;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -55,12 +54,10 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.PagedViewOrientedState;
 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.touch.PortraitPagedViewHandler;
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ActivityContext;
@@ -74,7 +71,9 @@
 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;
 
+    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;
 
@@ -92,14 +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;
 
     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;
 
@@ -121,13 +122,17 @@
     private float mLastMotion;
     private float mLastMotionRemainder;
     private float mTotalMotion;
-    protected PagedOrientationHandler mOrientationHandler = new PortraitPagedViewHandler();
-    protected final PagedViewOrientedState mOrientationState = new PagedViewOrientedState();
+    // 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;
 
@@ -144,9 +149,6 @@
 
     protected int mUnboundedScroll;
 
-    protected int mLayoutRotation = Surface.ROTATION_0;
-    protected int mDisplayRotation = Surface.ROTATION_0;
-
     // Page Indicator
     @Thunk int mPageIndicatorViewId;
     protected T mPageIndicator;
@@ -177,24 +179,19 @@
 
         setHapticFeedbackEnabled(false);
         mIsRtl = Utilities.isRtl(getResources());
-        init();
-    }
 
-    /**
-     * Initializes various states for this workspace.
-     */
-    protected void init() {
-        Context context = getContext();
         mScroller = new OverScroller(context);
         setDefaultInterpolator(Interpolators.SCROLL);
         mCurrentPage = 0;
 
         final ViewConfiguration configuration = ViewConfiguration.get(context);
-        mTouchSlop = configuration.getScaledPagingTouchSlop();
+        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);
 
@@ -264,13 +261,6 @@
         forceFinishScroller(true);
     }
 
-    /**
-     * Returns left offset of a page. This is the gap between pages and prevents overlap.
-     */
-    public int scrollOffsetLeft() {
-        return mInsets.left + getPaddingLeft();
-    }
-
     private void abortScrollerAnimation(boolean resetNextPage) {
         mScroller.abortAnimation();
         // We need to clean up the next page here to avoid computeScrollHelper from
@@ -417,37 +407,6 @@
         return mUnboundedScroll;
     }
 
-    protected void updateLayoutRotation(int touchRotation) {
-        setLayoutRotation(touchRotation, mDisplayRotation);
-    }
-
-    /** @param touchRotation Must be one of {@link android.view.Surface.ROTATION_0/90/180/270} */
-    public void setLayoutRotation(int touchRotation, int displayRotation) {
-        if (mLayoutRotation == touchRotation && mDisplayRotation == displayRotation) {
-            return;
-        }
-
-        mOrientationState.update(touchRotation, displayRotation);
-        mOrientationHandler = mOrientationState.getOrientationHandler();
-        mLayoutRotation = touchRotation;
-        mDisplayRotation = displayRotation;
-        requestLayout();
-    }
-
-    public PagedViewOrientedState getPagedViewOrientedState() {
-        return mOrientationState;
-    }
-
-    public PagedOrientationHandler getPagedOrientationHandler() {
-        return getPagedViewOrientedState().getOrientationHandler();
-    }
-
-    public void disableMultipleLayoutRotations(boolean disable) {
-        mOrientationState.disableMultipleOrientations(disable);
-        mOrientationHandler = mOrientationState.getOrientationHandler();
-        requestLayout();
-    }
-
     @Override
     public void scrollBy(int x, int y) {
         mOrientationHandler.delegateScrollBy(this, getUnboundedScroll(), x, y);
@@ -955,9 +914,10 @@
                 // Remember location of down touch
                 mDownMotionX = x;
                 mDownMotionY = y;
-                mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
+                mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
                 mLastMotionRemainder = 0;
                 mTotalMotion = 0;
+                mAllowEasyFling = false;
                 mActivePointerId = ev.getPointerId(0);
 
                 updateIsBeingDraggedOnTouchDown();
@@ -989,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;
@@ -1022,7 +982,7 @@
         final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
         final int diff = (int) Math.abs(primaryDirection - mLastMotion);
         final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
-        boolean moved = diff > touchSlop;
+        boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING;
 
         if (moved) {
             // Scroll if the user moved far enough along the X axis
@@ -1107,16 +1067,28 @@
         }
     }
 
+    /**
+     * 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) {
-        mSpringOverScroll = amount;
         if (amount == 0) {
             return;
         }
 
         int size = mOrientationHandler.getMeasuredSize(this);
         int overScrollAmount = OverScroll.dampedScroll(amount, size);
-        mSpringOverScroll = overScrollAmount;
         if (mScroller.isSpringing()) {
+            mSpringOverScroll = getSpringOverScroll(amount);
             invalidate();
             return;
         }
@@ -1128,8 +1100,8 @@
     }
 
     protected void overScroll(int amount) {
-        mSpringOverScroll = amount;
         if (mScroller.isSpringing()) {
+            mSpringOverScroll = getSpringOverScroll(amount);
             invalidate();
             return;
         }
@@ -1193,6 +1165,7 @@
             mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
             mLastMotionRemainder = 0;
             mTotalMotion = 0;
+            mAllowEasyFling = false;
             mActivePointerId = ev.getPointerId(0);
             if (mIsBeingDragged) {
                 onScrollInteractionBegin();
@@ -1200,8 +1173,14 @@
             }
             break;
 
-            case MotionEvent.ACTION_MOVE:
-                if (mIsBeingDragged) {
+        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
                 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
 
@@ -1231,6 +1210,8 @@
             if (mIsBeingDragged) {
                 final int activePointerId = mActivePointerId;
                 final int pointerIndex = ev.findPointerIndex(activePointerId);
+                if (pointerIndex == -1) return true;
+
                 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev,
                     pointerIndex);
                 final VelocityTracker velocityTracker = mVelocityTracker;
@@ -1245,9 +1226,14 @@
                     SIGNIFICANT_MOVE_THRESHOLD;
 
                 mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection);
-                boolean isFling = mTotalMotion > mTouchSlop && shouldFlingForVelocity(velocity);
+                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
@@ -1347,7 +1333,8 @@
     }
 
     protected boolean shouldFlingForVelocity(int velocity) {
-        return Math.abs(velocity) > mFlingThresholdVelocity;
+        float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
+        return Math.abs(velocity) > threshold;
     }
 
     private void resetTouchState() {
@@ -1424,8 +1411,7 @@
     }
 
     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
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 403d779..c9fb75a 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -22,6 +22,7 @@
 
 public class ResourceUtils {
     public static final int DEFAULT_NAVBAR_VALUE = 48;
+    public static final int INVALID_RESOURCE_HANDLE = -1;
     public static final String NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE = "navigation_bar_width";
     public static final String NAVBAR_BOTTOM_GESTURE_SIZE = "navigation_bar_gesture_height";
 
@@ -51,7 +52,13 @@
         return val;
     }
 
+    public static int getIntegerByName(String resName, Resources res, int defaultValue) {
+        int resId = res.getIdentifier(resName, "integer", "android");
+        return resId != 0 ? res.getInteger(resId) : defaultValue;
+    }
+
     public static int pxFromDp(float size, DisplayMetrics metrics) {
-        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+        return size < 0 ? INVALID_RESOURCE_HANDLE : 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 983c289..499b54f 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -3,12 +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;
@@ -34,7 +38,11 @@
 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;
@@ -55,7 +63,7 @@
 
     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;
 
@@ -66,8 +74,8 @@
 
     public SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-
         mCacheExpireAlarm = new Alarm();
+        mStatsLogManager = StatsLogManager.newInstance(context);
     }
 
     @Override
@@ -211,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
@@ -335,8 +350,12 @@
                     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/Utilities.java b/src/com/android/launcher3/Utilities.java
index 0cd08d4..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,10 +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;
@@ -60,12 +63,17 @@
 import android.view.ViewConfiguration;
 import android.view.animation.Interpolator;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
+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.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;
@@ -74,6 +82,7 @@
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -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 =
@@ -338,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.
      */
@@ -483,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.
      */
@@ -535,6 +606,7 @@
                 outObj[0] = activityInfo;
                 return activityInfo.getFullResIcon(appState.getIconCache());
             }
+            if (info.getIntent() == null || info.getIntent().getPackage() == null) return null;
             List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
                     .buildRequest(launcher)
                     .query(ShortcutRequest.ALL);
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 7c504a6..1441e0b 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -19,12 +19,18 @@
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 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;
@@ -76,9 +82,18 @@
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 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.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
@@ -109,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
@@ -238,6 +253,8 @@
     // Handles workspace state transitions
     private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
 
+    private final StatsLogManager mStatsLogManager;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -270,6 +287,7 @@
         // Disable multitouch across the workspace/all apps/customize tray
         setMotionEventSplittingEnabled(true);
         setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
+        mStatsLogManager = StatsLogManager.newInstance(context);
     }
 
     @Override
@@ -349,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);
         }
@@ -401,6 +419,9 @@
 
         // Always enter the spring loaded mode
         mLauncher.getStateManager().goToState(SPRING_LOADED);
+        mStatsLogManager.logger().withItemInfo(dragObject.dragInfo)
+                .withInstanceId(dragObject.logInstanceId)
+                .log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
     }
 
     public void deferRemoveExtraEmptyScreen() {
@@ -982,6 +1003,15 @@
             if (!mOverlayShown) {
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.LEFT, ContainerType.WORKSPACE, 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.
@@ -991,6 +1021,15 @@
                 if (!ued.isPreviousHomeGesture()) {
                     mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.RIGHT, ContainerType.WORKSPACE, -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
@@ -1082,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);
         }
     }
 
@@ -1203,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() {
@@ -1313,14 +1363,13 @@
 
         // 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);
         animation.add(stepAnimator);
     }
@@ -1331,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++) {
@@ -1417,6 +1469,10 @@
 
         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;
@@ -1427,12 +1483,8 @@
         Point dragVisualizeOffset = null;
         Rect dragRect = new Rect();
 
-        if (draggableView == null && child instanceof DraggableView) {
-            draggableView = (DraggableView) child;
-        }
-
         if (draggableView != null) {
-            draggableView.getVisualDragBounds(dragRect);
+            draggableView.getSourceVisualDragBounds(dragRect);
             dragLayerY += dragRect.top;
             dragVisualizeOffset = new Point(- halfPadding, halfPadding);
         }
@@ -1625,7 +1677,8 @@
             Rect folderLocation = new Rect();
             float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
             target.removeView(v);
-
+            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;
@@ -1663,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
@@ -1678,6 +1733,7 @@
     @Override
     public void prepareAccessibilityDrop() { }
 
+    @Override
     public void onDrop(final DragObject d, DragOptions options) {
         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
         CellLayout dropTargetLayout = mDropToLayout;
@@ -1864,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) {
@@ -2401,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
@@ -2489,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) {
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index 0b9d602..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 {
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index c4c4377..cd938e1 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -16,16 +16,22 @@
 
 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.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;
@@ -33,6 +39,7 @@
 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;
 
@@ -41,8 +48,11 @@
 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.
@@ -64,7 +74,7 @@
     }
 
     /**
-     * @see com.android.launcher3.LauncherStateManager.StateHandler#setStateWithAnimation
+     * @see com.android.launcher3.statemanager.StateManager.StateHandler#setStateWithAnimation
      */
     public void setStateWithAnimation(
             LauncherState toState, StateAnimationConfig config, PendingAnimation animation) {
@@ -102,17 +112,32 @@
         View qsbView = qsbScaleView.getSearchView();
         if (playAtomicComponent) {
             Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
-            propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
+            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 = config.getInterpolator(ANIM_HOTSEAT_SCALE,
-                    scaleInterpolator);
-            propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
-                    hotseatScaleInterpolator);
-            propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
-                    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);
@@ -161,7 +186,8 @@
         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) {
@@ -173,7 +199,8 @@
             PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter,
             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.onlyPlayAtomicComponent()) {
             // Don't update the scrim during the atomic animation.
@@ -187,4 +214,24 @@
                     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/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 414abab..136d43e 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -19,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;
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 8d1a102..c989e7b 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -16,9 +16,14 @@
 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;
@@ -37,12 +42,10 @@
 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;
@@ -50,11 +53,11 @@
 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.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
-import com.android.launcher3.pm.UserCache;
+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;
@@ -195,7 +198,7 @@
     }
 
     private void resetWorkProfile() {
-        mWorkModeSwitch.refresh();
+        mWorkModeSwitch.update(!mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED));
         mAH[AdapterHolder.WORK].setupOverlay();
         mAH[AdapterHolder.WORK].applyPadding();
     }
@@ -277,6 +280,10 @@
         }
     }
 
+    public LayoutInflater getLayoutInflater() {
+        return LayoutInflater.from(getContext());
+    }
+
     /**
      * Resets the state of AllApps.
      */
@@ -349,15 +356,15 @@
         }
 
         ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
+        mlp.leftMargin = insets.left;
+        mlp.rightMargin = insets.right;
+        setLayoutParams(mlp);
+
         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);
         }
-        setLayoutParams(mlp);
 
         InsettableFrameLayout.dispatchInsets(this, insets);
     }
@@ -427,11 +434,22 @@
     }
 
     private void setupWorkToggle() {
-        mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
-                R.layout.work_mode_switch, this, false);
-        this.addView(mWorkModeSwitch);
-        mWorkModeSwitch.setInsets(mInsets);
-        mWorkModeSwitch.post(() -> mAH[AdapterHolder.WORK].applyPadding());
+        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) {
@@ -444,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;
@@ -466,7 +484,9 @@
         }
         reset(true /* animate */);
         if (mWorkModeSwitch != null) {
-            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK);
+            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK
+                    && mAllAppsStore.hasModelFlag(
+                            FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
         }
     }
 
@@ -576,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;
                 }
@@ -617,7 +630,7 @@
         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();
         }
@@ -643,18 +656,20 @@
 
         void setupOverlay() {
             if (!mIsWork || recyclerView == null) return;
-            boolean workDisabled = UserCache.INSTANCE.get(mLauncher).isAnyProfileQuietModeEnabled();
+            boolean workDisabled = mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
             if (mWorkDisabled == workDisabled) return;
-            recyclerView.setContentDescription(
-                    workDisabled ? mLauncher.getString(R.string.work_apps_paused_title) : null);
+            recyclerView.setContentDescription(workDisabled ? mLauncher.getString(
+                    R.string.work_apps_paused_content_description) : null);
             View overlayView = getOverlayView();
             recyclerView.setItemAnimator(new DefaultItemAnimator());
             if (workDisabled) {
                 overlayView.setAlpha(0);
-                appsList.updateItemFilter((info, cn) -> false);
                 recyclerView.addAutoSizedOverlay(overlayView);
                 overlayView.animate().alpha(1).withEndAction(
-                        () -> recyclerView.setItemAnimator(null)).start();
+                        () -> {
+                            appsList.updateItemFilter((info, cn) -> false);
+                            recyclerView.setItemAnimator(null);
+                        }).start();
             } else if (mInfoMatcher != null) {
                 appsList.updateItemFilter(mInfoMatcher);
                 overlayView.animate().alpha(0).withEndAction(() -> {
@@ -667,8 +682,12 @@
 
         void applyPadding() {
             if (recyclerView != null) {
-                int bottomOffset =
-                        mWorkModeSwitch != null && mIsWork ? mWorkModeSwitch.getHeight() : 0;
+                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);
             }
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 1f861bc..8ec4d27 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -37,12 +37,12 @@
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.launcher3.AppInfo;
 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;
@@ -71,11 +71,6 @@
     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.
      */
@@ -186,7 +181,6 @@
 
     private int mAppsPerRow;
 
-    private BindViewCallback mBindViewCallback;
     private OnFocusChangeListener mIconFocusListener;
 
     // The text to show when there are no search results and no market search handler.
@@ -194,7 +188,8 @@
     // The intent to send off to the market app, updated each time the search query changes.
     private Intent mMarketSearchIntent;
 
-    public AllAppsGridAdapter(BaseDraggingActivity launcher, AlphabeticalAppsList apps) {
+    public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
+            AlphabeticalAppsList apps) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -202,7 +197,7 @@
         mGridSizer = new GridSpanSizer();
         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
-        mLayoutInflater = LayoutInflater.from(launcher);
+        mLayoutInflater = inflater;
 
         mOnIconClickListener = launcher.getItemOnClickListener();
 
@@ -248,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() {
@@ -319,9 +307,6 @@
                 // nothing to do
                 break;
         }
-        if (mBindViewCallback != null) {
-            mBindViewCallback.onBindView(holder);
-        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 8fe4633..cbf02b7 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -35,11 +35,11 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
 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;
@@ -53,12 +53,12 @@
 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;
@@ -85,6 +85,7 @@
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
         mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+        mFastScrollHelper = new AllAppsFastScrollHelper(this);
     }
 
     /**
@@ -92,7 +93,6 @@
      */
     public void setApps(AlphabeticalAppsList apps) {
         mApps = apps;
-        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
     public AlphabeticalAppsList getApps() {
@@ -221,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();
@@ -236,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;
     }
 
@@ -257,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 a6ef10a..3ae0a18 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -15,23 +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.util.Log;
 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;
 
@@ -50,14 +50,13 @@
 
     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;
 
-    private boolean mListenerUpdateInProgress = false;
-
     public AppInfo[] getApps() {
         return mApps;
     }
@@ -65,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;
@@ -102,12 +111,9 @@
             mUpdatePending = true;
             return;
         }
-        mListenerUpdateInProgress = true;
-        int count = mUpdateListeners.size();
-        for (int i = 0; i < count; i++) {
-            mUpdateListeners.get(i).onAppsUpdated();
+        for (OnUpdateListener listener : mUpdateListeners) {
+            listener.onAppsUpdated();
         }
-        mListenerUpdateInProgress = false;
     }
 
     public void addUpdateListener(OnUpdateListener listener) {
@@ -115,9 +121,6 @@
     }
 
     public void removeUpdateListener(OnUpdateListener listener) {
-        if (mListenerUpdateInProgress) {
-            Log.e("AllAppsStore", "Trying to remove listener during update", new Exception());
-        }
         mUpdateListeners.remove(listener);
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 68b0706..a9b030e 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -6,32 +6,39 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 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.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 static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
 
 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.StateHandler;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 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.Themes;
+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.
@@ -43,7 +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 {
+public class AllAppsTransitionController implements StateHandler<LauncherState>,
+        OnDeviceProfileChangeListener, PluginListener<AllAppsSearchPlugin> {
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -65,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}.
@@ -79,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);
     }
@@ -120,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 = Math.min(shiftCurrent, mScrimView.getVisualTop())
-                <= mLauncher.getDeviceProfile().getInsets().top / 2f;
-        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);
         }
     }
 
@@ -156,7 +159,9 @@
             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;
@@ -172,7 +177,6 @@
                 : FAST_OUT_SLOW_IN;
 
         Animator anim = createSpringAnimation(mProgress, targetProgress);
-        anim.setDuration(config.duration);
         anim.setInterpolator(config.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
         anim.addListener(getProgressAnimatorListener());
         builder.add(anim);
@@ -196,16 +200,25 @@
 
         Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
         Interpolator headerFade = config.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);
+
+        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);
 
-        setter.setViewAlpha(mAppsView, hasAnyVisibleItem ? 1 : 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() {
@@ -215,6 +228,8 @@
     public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
         mAppsView = appsView;
         mScrimView = scrimView;
+        PluginManagerWrapper.INSTANCE.get(mLauncher)
+                .addPluginListener(this, AllAppsSearchPlugin.class, false);
     }
 
     /**
@@ -237,5 +252,47 @@
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
         }
+        updatePluginAnimationEnd();
+    }
+
+    @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);
+    }
+
+    @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 b501c82..06209bb 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -18,8 +18,8 @@
 
 import android.content.Context;
 
-import com.android.launcher3.AppInfo;
 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;
diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java
index 8baf56c..823f98e 100644
--- a/src/com/android/launcher3/allapps/AppInfoComparator.java
+++ b/src/com/android/launcher3/allapps/AppInfoComparator.java
@@ -19,7 +19,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 
-import com.android.launcher3.AppInfo;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.LabelComparator;
 
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index 0f0fc3a..b4ff5ea 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -24,7 +24,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
-import android.content.SharedPreferences;
 import android.os.Handler;
 import android.os.UserManager;
 import android.view.MotionEvent;
@@ -32,9 +31,11 @@
 import com.android.launcher3.AbstractFloatingView;
 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.Utilities;
+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
@@ -43,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);
@@ -142,8 +136,9 @@
     }
 
     private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) {
+        OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
         if (!launcher.isInState(NORMAL)
-                || launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)
+                || onboardingPrefs.getBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN)
                 || AbstractFloatingView.getTopOpenView(launcher) != null
                 || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
@@ -154,35 +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)
+                || !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 (Launcher.ACTIVITY_TRACKER.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);
@@ -209,22 +208,4 @@
             mController.setProgress(progress - mDelta);
         }
     }
-
-    private static void incrementShelfBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply();
-    }
-
-    private static void incrementHomeBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply();
-    }
 }
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index f6766c4..a6bc6cf 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -22,7 +22,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.views.WorkEduView;
 
 /**
@@ -32,7 +32,7 @@
 
     private final Launcher mLauncher;
 
-    private LauncherStateManager.StateListener mWorkTabListener;
+    private StateListener<LauncherState> mWorkTabListener;
 
     public LauncherAllAppsContainerView(Context context) {
         this(context, null);
@@ -60,6 +60,14 @@
     }
 
     @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()
diff --git a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
index 2515c24..2de425e 100644
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
@@ -135,8 +135,8 @@
     @Override
     public void setActiveMarker(int activePage) {
         updateTabTextColor(activePage);
-        updateIndicatorPosition(activePage);
         if (mContainerView != null && mLastActivePage != activePage) {
+            updateIndicatorPosition(activePage);
             mContainerView.onTabChanged(activePage);
         }
         mLastActivePage = activePage;
diff --git a/src/com/android/launcher3/allapps/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 05db18e..4567ee6 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -15,20 +15,19 @@
  */
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
 import android.content.Context;
-import android.content.pm.PackageManager;
+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.Insettable;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
@@ -46,48 +45,66 @@
 
     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) {
-
-    }
+    public void setChecked(boolean checked) { }
 
     @Override
     public void toggle() {
-        Launcher launcher = Launcher.getLauncher(getContext());
         // don't show tip if user uses toggle
-        launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
+        Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
         trySetQuietModeEnabledToAllProfilesAsync(isChecked());
     }
 
-    private void setCheckedInternal(boolean checked) {
-        super.setChecked(checked);
-        setCompoundDrawablesWithIntrinsicBounds(
-                checked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
-    }
-
-    public void refresh() {
-        if (!shouldShowWorkSwitch()) return;
-        UserCache userManager = UserCache.INSTANCE.get(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
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        this.setVisibility(shouldShowWorkSwitch() ? VISIBLE : GONE);
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mTouch[0] = ev.getX();
+            mTouch[1] = ev.getY();
+        } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
+            if (Math.abs(mTouch[0] - ev.getX()) > mTouchSlop
+                    || Math.abs(mTouch[1] - ev.getY()) > mTouchSlop) {
+                int action = ev.getAction();
+                ev.setAction(MotionEvent.ACTION_CANCEL);
+                super.onTouchEvent(ev);
+                ev.setAction(action);
+                return false;
+            }
+        }
+        return super.onTouchEvent(ev);
     }
 
     private void trySetQuietModeEnabledToAllProfilesAsync(boolean enabled) {
@@ -106,13 +123,12 @@
      * Animates in/out work profile toggle panel based on the tab user is on
      */
     public void setWorkTabVisible(boolean workTabVisible) {
-        if (!shouldShowWorkSwitch()) return;
         clearAnimation();
         if (workTabVisible) {
             setVisibility(VISIBLE);
             setAlpha(0);
             animate().alpha(1).start();
-            showTipifNeeded();
+            showTipIfNeeded();
         } else {
             animate().alpha(0).withEndAction(() -> this.setVisibility(GONE)).start();
         }
@@ -169,23 +185,18 @@
         }
     }
 
-    private boolean shouldShowWorkSwitch() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
-                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
-                == PackageManager.PERMISSION_GRANTED);
-    }
-
     /**
      * Shows a work tip on the Nth work tab open
      */
-    public void showTipifNeeded() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        int tipCounter = launcher.getSharedPrefs().getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
+    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(launcher).show(launcher.getString(R.string.work_switch_tip), getTop());
+            new ArrowTipView(context)
+                    .show(context.getString(R.string.work_switch_tip), getTop());
         }
-        launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
+        prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 9e3a862..356c52c 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -19,7 +19,6 @@
 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;
 
@@ -33,6 +32,7 @@
 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;
@@ -212,6 +212,11 @@
     @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/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index f12789a..ea0ff8b 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -15,6 +15,7 @@
  */
 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.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
@@ -27,12 +28,9 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.util.FloatProperty;
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.Utilities;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -55,28 +53,12 @@
      * to float (animation-fraction * total duration) to int conversion.
      */
     public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
-        /**
-         * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
-         */
         ArrayList<Holder> childAnims = new ArrayList<>();
-        addAnimationHoldersRecur(anim, SpringProperty.DEFAULT, childAnims);
+        addAnimationHoldersRecur(anim, duration, SpringProperty.DEFAULT, childAnims);
 
         return new AnimatorPlaybackController(anim, duration, childAnims);
     }
 
-    private static final FloatProperty<ValueAnimator> CURRENT_PLAY_TIME =
-            new FloatProperty<ValueAnimator>("current-play-time") {
-                @Override
-                public void setValue(ValueAnimator animator, float v) {
-                    animator.setCurrentPlayTime((long) v);
-                }
-
-                @Override
-                public Float get(ValueAnimator animator) {
-                    return (float) animator.getCurrentPlayTime();
-                }
-            };
-
     // Progress factor after which an animation is considered almost completed.
     private static final float ANIMATION_COMPLETE_THRESHOLD = 0.95f;
 
@@ -166,7 +148,7 @@
         float scaleInverse = 1 / Math.abs(scale);
         float scaledVelocity = velocity * scaleInverse;
 
-        float nextFrameProgress = Utilities.boundToRange(getProgressFraction()
+        float nextFrameProgress = boundToRange(getProgressFraction()
                 + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
 
         // Update setters for spring
@@ -177,21 +159,22 @@
         long springDuration = animationDuration;
         for (Holder h : mChildAnimations) {
             if ((h.springProperty.flags & springFlag) != 0) {
-                SpringAnimationBuilder s = new SpringAnimationBuilder(h.anim, CURRENT_PLAY_TIME)
-                        .setStartValue(clampDuration(mCurrentFraction))
-                        .setEndValue(goingToEnd ? h.anim.getDuration() : 0)
-                        .setStartVelocity(scaledVelocity * h.anim.getDuration())
+                SpringAnimationBuilder s = new SpringAnimationBuilder(context)
+                        .setStartValue(mCurrentFraction)
+                        .setEndValue(goingToEnd ? 1 : 0)
+                        .setStartVelocity(scaledVelocity)
                         .setMinimumVisibleChange(scaleInverse)
                         .setDampingRatio(h.springProperty.mDampingRatio)
-                        .setStiffness(h.springProperty.mStiffness);
+                        .setStiffness(h.springProperty.mStiffness)
+                        .computeParams();
 
-                long expectedDurationL = s.build(context).getDuration();
+                long expectedDurationL = s.getDuration();
                 springDuration = Math.max(expectedDurationL, springDuration);
 
                 float expectedDuration = expectedDurationL;
-                h.setter = (a, l) ->
-                    s.setValue(a, mAnimationPlayer.getCurrentPlayTime() / expectedDuration);
-                h.anim.setInterpolator(LINEAR);
+                h.mapper = (progress, globalEndProgress) ->
+                        mAnimationPlayer.getCurrentPlayTime() / expectedDuration;
+                h.anim.setInterpolator(s::getInterpolatedValue);
             }
         }
 
@@ -250,9 +233,9 @@
         if (mTargetCancelled) {
             return;
         }
-        long playPos = clampDuration(fraction);
+        float progress = boundToRange(fraction, 0, 1);
         for (Holder holder : mChildAnimations) {
-            holder.setter.set(holder.anim, playPos);
+            holder.setProgress(progress);
         }
     }
 
@@ -374,14 +357,14 @@
     }
 
     /**
-     * Interface for setting position of value animator
+     * Interface for mapping progress to animation progress
      */
-    private interface PositionSetter {
+    private interface ProgressMapper {
 
-        PositionSetter DEFAULT = (anim, playPos) ->
-                anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration()));
+        ProgressMapper DEFAULT = (progress, globalEndProgress) ->
+                progress > globalEndProgress ? 1 : (progress / globalEndProgress);
 
-        void set(ValueAnimator anim, long position);
+        float getProgress(float progress, float globalProgress);
     }
 
     /**
@@ -395,27 +378,34 @@
 
         public final TimeInterpolator interpolator;
 
-        public PositionSetter setter;
+        public final float globalEndProgress;
 
-        Holder(Animator anim, SpringProperty springProperty) {
+        public ProgressMapper mapper;
+
+        Holder(Animator anim, float globalDuration, SpringProperty springProperty) {
             this.anim = (ValueAnimator) anim;
             this.springProperty = springProperty;
             this.interpolator = this.anim.getInterpolator();
-            this.setter = PositionSetter.DEFAULT;
+            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);
-            setter = PositionSetter.DEFAULT;
+            mapper = ProgressMapper.DEFAULT;
         }
     }
 
-    static void addAnimationHoldersRecur(
-            Animator anim, SpringProperty springProperty, ArrayList<Holder> out) {
+    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, springProperty));
+            out.add(new Holder(anim, globalDuration, springProperty));
         } else if (anim instanceof AnimatorSet) {
             for (Animator child : ((AnimatorSet) anim).getChildAnimations()) {
                 if (forceDuration > 0) {
@@ -424,7 +414,7 @@
                 if (forceInterpolator != null) {
                     child.setInterpolator(forceInterpolator);
                 }
-                addAnimationHoldersRecur(child, springProperty, out);
+                addAnimationHoldersRecur(child, globalDuration, springProperty, out);
             }
         } else {
             throw new RuntimeException("Unknown animation type " + anim);
diff --git a/src/com/android/launcher3/anim/FlingSpringAnim.java b/src/com/android/launcher3/anim/FlingSpringAnim.java
index 06d0f1c..6ea38ec 100644
--- a/src/com/android/launcher3/anim/FlingSpringAnim.java
+++ b/src/com/android/launcher3/anim/FlingSpringAnim.java
@@ -35,6 +35,7 @@
 
     private final FlingAnimation mFlingAnim;
     private SpringAnimation mSpringAnim;
+    private final boolean mSkipFlingAnim;
 
     private float mTargetPosition;
 
@@ -57,6 +58,10 @@
                 .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)
@@ -84,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
index a95a5e1..4195933 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -18,6 +18,7 @@
 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;
@@ -51,12 +52,8 @@
     private ValueAnimator mProgressAnimator;
 
     public PendingAnimation(long  duration) {
-        this(duration, new AnimatorSet());
-    }
-
-    public PendingAnimation(long  duration, AnimatorSet targetSet) {
         mDuration = duration;
-        mAnim = targetSet;
+        mAnim = new AnimatorSet();
     }
 
     /**
@@ -72,8 +69,8 @@
     }
 
     public void add(Animator a, SpringProperty springProperty) {
-        mAnim.play(a);
-        addAnimationHoldersRecur(a, springProperty, mAnimHolders);
+        mAnim.play(a.setDuration(mDuration));
+        addAnimationHoldersRecur(a, mDuration, springProperty, mAnimHolders);
     }
 
     public void finish(boolean isSuccess, int logAction) {
@@ -90,7 +87,7 @@
         }
         ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha);
         anim.addListener(new AlphaUpdateListener(view));
-        anim.setDuration(mDuration).setInterpolator(interpolator);
+        anim.setInterpolator(interpolator);
         add(anim);
     }
 
@@ -105,6 +102,13 @@
         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) {
@@ -112,7 +116,7 @@
             return;
         }
         Animator anim = ObjectAnimator.ofInt(target, property, value);
-        anim.setDuration(mDuration).setInterpolator(interpolator);
+        anim.setInterpolator(interpolator);
         add(anim);
     }
 
@@ -121,14 +125,33 @@
      */
     public void addOnFrameCallback(Runnable runnable) {
         if (mProgressAnimator == null) {
-            mProgressAnimator = ValueAnimator.ofFloat(0, 1).setDuration(mDuration);
-            add(mProgressAnimator);
+            mProgressAnimator = ValueAnimator.ofFloat(0, 1);
         }
 
         mProgressAnimator.addUpdateListener(anim -> runnable.run());
     }
 
-    public AnimatorSet getAnim() {
+    /**
+     * @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;
     }
 
@@ -136,7 +159,7 @@
      * Creates a controller for this animation
      */
     public AnimatorPlaybackController createPlaybackController() {
-        return new AnimatorPlaybackController(mAnim, mDuration, mAnimHolders);
+        return new AnimatorPlaybackController(buildAnim(), mDuration, mAnimHolders);
     }
 
     /**
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index f22a9f0..a9702b4 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -15,7 +15,10 @@
  */
 package com.android.launcher3.anim;
 
-import android.animation.ObjectAnimator;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.FloatProperty;
 
@@ -28,10 +31,9 @@
  * 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;
@@ -64,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];
@@ -94,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.");
@@ -103,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) {
@@ -113,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.");
@@ -122,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);
 
@@ -187,12 +181,27 @@
             }
         } 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);
-        animator.addListener(AnimationSuccessListener.forRunnable(
-                () -> mProperty.setValue(mTarget, mEndValue)));
+    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 animation) {
+                property.set(target, mEndValue);
+            }
+        });
         return animator;
     }
 
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 737c97b..1d32d1d 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -75,9 +75,6 @@
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendScrollFinishedEventToTest");
-        }
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index ec34350..5c4a492 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -36,8 +36,6 @@
     private static final List<DebugFlag> sDebugFlags = new ArrayList<>();
 
     public static final String FLAGS_PREF_NAME = "featureFlags";
-    public static final String FLAG_ENABLE_FIXED_ROTATION_TRANSFORM =
-            "ENABLE_FIXED_ROTATION_TRANSFORM";
 
     private FeatureFlags() { }
 
@@ -92,23 +90,31 @@
     public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag(
             "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
 
-    public static final BooleanFlag ENABLE_HINTS_IN_OVERVIEW = getDebugFlag(
-            "ENABLE_HINTS_IN_OVERVIEW", false, "Show chip hints and gleams on the overview screen");
+    // 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", false, "Allow option to dimiss apps from predicted list");
+            "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");
@@ -117,30 +123,31 @@
             "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 = new DeviceFlag(
-            "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
+    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 = new DeviceFlag(
+    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", false, "Use the multi-db grid migration algorithm");
+            "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 USE_SURFACE_VIEW_FOR_GRID_PREVIEW = getDebugFlag(
-            "USE_SURFACE_VIEW_FOR_GRID_PREVIEW", true, "Use surface view for grid preview");
-
     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.");
 
-    public static final BooleanFlag ENABLE_SELECT_MODE = getDebugFlag(
-            "ENABLE_SELECT_MODE", true, "Show Select Mode button in Overview Actions");
+    // 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,
@@ -159,15 +166,23 @@
             "ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS", false,
             "Always use hardware optimization for folder animations.");
 
-    public static final BooleanFlag ENABLE_FIXED_ROTATION_TRANSFORM = getDebugFlag(
-            FLAG_ENABLE_FIXED_ROTATION_TRANSFORM, true,
-            "Launch/close apps without rotation animation. Fix Launcher to portrait");
+    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));
         }
     }
 
@@ -178,10 +193,20 @@
     }
 
     public static void dump(PrintWriter pw) {
-        pw.println("FeatureFlags:");
+        pw.println("DeviceFlags:");
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
-                pw.println("  " + flag.key + "=" + flag.get());
+                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());
+                }
             }
         }
     }
@@ -202,13 +227,11 @@
 
         @Override
         public String toString() {
-            return appendProps(new StringBuilder()
-                    .append(getClass().getSimpleName()).append('{'))
-                    .append('}').toString();
+            return appendProps(new StringBuilder()).toString();
         }
 
         protected StringBuilder appendProps(StringBuilder src) {
-            return src.append("key=").append(key).append(", defaultValue=").append(defaultValue);
+            return src.append(key).append(", defaultValue=").append(defaultValue);
         }
 
         public void addChangeListener(Context context, Runnable r) { }
@@ -240,8 +263,7 @@
 
         @Override
         protected StringBuilder appendProps(StringBuilder src) {
-            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue)
-                    .append(", description=").append(description);
+            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue);
         }
     }
 
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 6c40b8a..0df6713 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -170,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));
-
-        Launcher.ACTIVITY_TRACKER.schedule(listener);
+                        .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;
     }
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 9b91a1d..707fd06 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -92,7 +92,7 @@
             postCleanup();
             return false;
         }
-        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED || !mDragController.isDragging()) {
             if (onDragStart(event)) {
                 return true;
             } else {
@@ -161,7 +161,6 @@
     }
 
     protected void postCleanup() {
-        Launcher.ACTIVITY_TRACKER.clearReference(this);
         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 db61f59..03028d3 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -37,11 +37,11 @@
 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.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.TouchController;
 
@@ -186,8 +186,7 @@
 
         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));
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 970c5a0..ddf44ca 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -274,19 +274,29 @@
         scale *= childScale;
         int toX = Math.round(coord[0]);
         int toY = Math.round(coord[1]);
+
         float toScale = scale;
 
         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;
-            d.getVisualDragBounds(dragViewBounds);
+            Rect destRect = new Rect();
+            d.getWorkspaceVisualDragBounds(destRect);
+
+            // 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 - scale) / 2;
-            float scaleShiftY = dragView.getMeasuredHeight() * (1 - scale) / 2;
+            float scaleShiftX = dragView.getMeasuredWidth() * (1 - toScale) / 2;
+            float scaleShiftY = dragView.getMeasuredHeight() * (1 - toScale) / 2;
 
-            toX += scale * (dragViewBounds.left - dragView.getBlurSizeOutline() / 2) - scaleShiftX;
-            toY += scale * (dragViewBounds.top - dragView.getBlurSizeOutline() / 2) - scaleShiftY;
+            toX += scale * destRect.left - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftX;
+            toY += scale * destRect.top - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftY;
         }
 
         child.setVisibility(INVISIBLE);
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 7c76d34..de0fa1a 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -46,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();
 
@@ -176,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);
diff --git a/src/com/android/launcher3/dragndrop/DraggableView.java b/src/com/android/launcher3/dragndrop/DraggableView.java
index df99902..f7dcf6b 100644
--- a/src/com/android/launcher3/dragndrop/DraggableView.java
+++ b/src/com/android/launcher3/dragndrop/DraggableView.java
@@ -18,6 +18,10 @@
 
 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
@@ -42,9 +46,12 @@
     int getViewType();
 
     /**
-     * Before rendering as a DragView bitmap, some views need a preparation step.
+     * Before rendering as a DragView bitmap, some views need a preparation step. Returns a
+     * callback to clear any preparation work
      */
-    default void prepareDrawDragView() { }
+    @NonNull default SafeCloseable prepareDrawDragView() {
+        return () -> { };
+    }
 
     /**
      * If an actual View subclass, this method returns the rectangle (within the View's coordinates)
@@ -53,5 +60,14 @@
      *
      * @param bounds Visual bounds in the views coordinates will be written here.
      */
-    default void getVisualDragBounds(Rect bounds) { }
+    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/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 77c6306..bf3aa7f 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -30,10 +30,10 @@
 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.model.data.ItemInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index f71cfb8..9982b39 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -35,8 +35,8 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
 
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 202836d..d01e189 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -18,24 +18,14 @@
 
 import static android.text.TextUtils.isEmpty;
 
-import static androidx.core.util.Preconditions.checkNotNull;
-
-import static com.android.launcher3.FolderInfo.FLAG_MANUAL_FOLDER_NAME;
 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.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 static java.util.Arrays.asList;
-import static java.util.Arrays.stream;
-import static java.util.Optional.ofNullable;
+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;
@@ -43,7 +33,6 @@
 import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
@@ -65,16 +54,12 @@
 
 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;
@@ -84,7 +69,6 @@
 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;
@@ -92,12 +76,16 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
+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.LauncherLogProto.Action;
-import com.android.launcher3.userevent.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.LauncherLogProto.LauncherEvent;
-import com.android.launcher3.userevent.LauncherLogProto.Target;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
@@ -109,9 +97,9 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
+import java.util.StringJoiner;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
  * Represents a set of icons chosen by the user or generated by the system.
@@ -121,6 +109,12 @@
         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
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
@@ -162,10 +156,13 @@
     @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;
 
@@ -209,8 +206,7 @@
     @Thunk int mScrollHintDir = SCROLL_NONE;
     @Thunk int mCurrentScrollDir = SCROLL_NONE;
 
-    private String mPreviousLabel;
-    private boolean mIsPreviousLabelSuggested;
+    private StatsLogManager mStatsLogManager;
 
     /**
      * Used to inflate the Workspace from XML.
@@ -223,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
@@ -325,11 +323,7 @@
     public void startEditingFolderName() {
         post(() -> {
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                ofNullable(mInfo)
-                        .map(info -> info.suggestedFolderNames)
-                        .map(folderNames -> (FolderNameInfo[]) folderNames
-                                .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-                        .ifPresent(nameInfos -> showLabelSuggestion(nameInfos, false));
+                showLabelSuggestions();
             }
             mFolderName.setHint("");
             mIsEditingName = true;
@@ -344,12 +338,8 @@
         if (DEBUG) {
             Log.d(TAG, "onBackKey newTitle=" + newTitle);
         }
-
-        mInfo.title = newTitle;
-        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, getAcceptedSuggestionIndex() < 0,
-                mLauncher.getModelWriter());
+        mInfo.setTitle(newTitle, mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
-        mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
         if (TextUtils.isEmpty(mInfo.title)) {
             mFolderName.setHint(R.string.folder_hint_text);
@@ -425,6 +415,8 @@
 
     void bind(FolderInfo info) {
         mInfo = info;
+        mFromTitle = info.title;
+        mFromLabelState = info.getFromLabelState();
         ArrayList<WorkspaceItemInfo> children = info.contents;
         Collections.sort(children, ITEM_POS_COMPARATOR);
         updateItemLocationsInDatabaseBatch(true);
@@ -437,8 +429,6 @@
         }
         mItemsInvalidated = true;
         mInfo.addListener(this);
-        Optional.ofNullable(mInfo.title).ifPresent(title -> mPreviousLabel = title.toString());
-        mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
 
         if (!isEmpty(mInfo.title)) {
             mFolderName.setText(mInfo.title);
@@ -455,51 +445,29 @@
         });
     }
 
-    /**
-     * Show suggested folder title in FolderEditText, push InputMethodManager suggestions and save
-     * the suggestedFolderNames.
-     */
-    public void showSuggestedTitle(FolderNameInfo[] nameInfos) {
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-            if (isEmpty(mFolderName.getText().toString())
-                    && !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME)) {
-                showLabelSuggestion(nameInfos, true);
-            }
-        }
-    }
 
     /**
      * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
-     * InputMethodManager suggestions.
+     * rest of the suggestions to InputMethodManager.
      */
-    private void showLabelSuggestion(FolderNameInfo[] nameInfos, boolean animate) {
-        if (nameInfos == null) {
+    private void showLabelSuggestions() {
+        if (mInfo.suggestedFolderNames == null) {
             return;
         }
-        // Open the Folder and Keyboard when the first or second suggestion is valid non-empty
-        // string.
-        boolean shouldOpen = nameInfos.length > 0 && nameInfos[0] != null && !isEmpty(
-                nameInfos[0].getLabel())
-                || nameInfos.length > 1 && nameInfos[1] != null && !isEmpty(
-                nameInfos[1].getLabel());
-
-        if (shouldOpen) {
+        if (mInfo.suggestedFolderNames.hasSuggestions()) {
             // update the primary suggestion if the folder name is empty.
             if (isEmpty(mFolderName.getText())) {
-                CharSequence firstLabel = nameInfos[0] == null ? "" : nameInfos[0].getLabel();
-                if (!isEmpty(firstLabel)) {
+                if (mInfo.suggestedFolderNames.hasPrimary()) {
                     mFolderName.setHint("");
-                    mFolderName.setText(firstLabel);
+                    mFolderName.setText(mInfo.suggestedFolderNames.getLabels()[0]);
+                    mFolderName.selectAll();
                 }
             }
-            if (animate) {
-                animateOpen(mInfo.contents, 0, true);
-            }
             mFolderName.showKeyboard();
             mFolderName.displayCompletions(
-                    asList(nameInfos).subList(0, nameInfos.length).stream()
+                    Stream.of(mInfo.suggestedFolderNames.getLabels())
                             .filter(Objects::nonNull)
-                            .map(s -> s.getLabel().toString())
+                            .map(Object::toString)
                             .filter(s -> !s.isEmpty())
                             .filter(s -> !s.equalsIgnoreCase(mFolderName.getText().toString()))
                             .collect(Collectors.toList()));
@@ -742,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);
@@ -762,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();
@@ -1017,16 +999,14 @@
         if (!items.isEmpty()) {
             mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
         }
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind) {
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind
+                && total > 1 /* no need to update if there's one icon */) {
             Executors.MODEL_EXECUTOR.post(() -> {
-                FolderNameInfo[] nameInfos =
-                        new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+                FolderNameInfos nameInfos = new FolderNameInfos();
                 FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
                 fnp.getSuggestedFolderName(
                         getContext(), mInfo.contents, nameInfos);
-                mInfo.suggestedFolderNames = new Intent().putExtra(
-                        FolderInfo.EXTRA_FOLDER_SUGGESTIONS,
-                        nameInfos);
+                mInfo.suggestedFolderNames = nameInfos;
             });
         }
     }
@@ -1349,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
@@ -1451,9 +1433,38 @@
     public void onFocusChange(View v, boolean hasFocus) {
         if (v == mFolderName) {
             if (hasFocus) {
+                mFromLabelState = mInfo.getFromLabelState();
+                mFromTitle = mInfo.title;
                 startEditingFolderName();
             } else {
-                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();
             }
         }
@@ -1652,127 +1663,14 @@
         return mContent;
     }
 
-    private void logEditFolderLabel() {
-        LauncherEvent launcherEvent = LauncherEvent.newBuilder()
-                .setAction(Action.newBuilder().setType(Action.Type.SOFT_KEYBOARD))
-                .addSrcTarget(newEditTextTargetBuilder()
-                        .setFromFolderLabelState(getFromFolderLabelState())
-                        .setToFolderLabelState(getToFolderLabelState()))
-                .addSrcTarget(newFolderTargetBuilder())
-                .addSrcTarget(newParentContainerTarget())
-                .build();
-        mLauncher.getUserEventDispatcher().logLauncherEvent(launcherEvent);
-        mPreviousLabel = mFolderName.getText().toString();
-        mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
-    }
-
-    private Target.FromFolderLabelState getFromFolderLabelState() {
-        return mPreviousLabel == null
-                ? FROM_FOLDER_LABEL_STATE_UNSPECIFIED
-                : mPreviousLabel.isEmpty()
-                ? FROM_EMPTY
-                : mIsPreviousLabelSuggested
-                ? FROM_SUGGESTED
-                : FROM_CUSTOM;
-    }
-
-    private Target.ToFolderLabelState getToFolderLabelState() {
-        String newLabel =
-                checkNotNull(mFolderName.getText().toString(),
-                        "Expected valid folder label, but found null");
-
-        Optional<String[]> suggestedLabels = getSuggestedLabels();
-        int accepted_suggestion_index = getAcceptedSuggestionIndex();
-        boolean hasValidPrimary = suggestedLabels
-                .map(labels -> labels.length > 0 && !isEmpty(labels[0]))
-                .orElse(false);
-        String primarySuffix = hasValidPrimary
-                ? "_WITH_VALID_PRIMARY"
-                : "_WITH_EMPTY_PRIMARY";
-
-        boolean isEmptySuggestions = suggestedLabels
-                .map(labels -> stream(labels).allMatch(TextUtils::isEmpty))
-                .orElse(true);
-        boolean isSuggestionsEnabled = FeatureFlags.FOLDER_NAME_SUGGEST.get();
-        String suggestionsSuffix =  isSuggestionsEnabled
-                ? isEmptySuggestions
-                    ? "_WITH_EMPTY_SUGGESTIONS"
-                    : "_WITH_VALID_SUGGESTIONS"
-                : "_WITH_SUGGESTIONS_DISABLED";
-
-        return newLabel.equals(mPreviousLabel)
-                ? Target.ToFolderLabelState.UNCHANGED
-                : newLabel.isEmpty()
-                    ? Target.ToFolderLabelState.valueOf("TO_EMPTY" + suggestionsSuffix)
-                    : accepted_suggestion_index >= 0
-                        ? Target.ToFolderLabelState.valueOf("TO_SUGGESTION"
-                            + accepted_suggestion_index
-                            + primarySuffix)
-                        : Target.ToFolderLabelState.valueOf("TO_CUSTOM" + suggestionsSuffix);
-    }
-
-    private Optional<String[]> getSuggestedLabels() {
-        return ofNullable(mInfo)
-            .map(info -> info.suggestedFolderNames)
-            .map(
-                folderNames ->
-                    (FolderNameInfo[])
-                        folderNames.getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-            .map(
-                folderNameInfoArray ->
-                    stream(folderNameInfoArray)
-                        .filter(Objects::nonNull)
-                        .map(FolderNameInfo::getLabel)
-                        .filter(Objects::nonNull)
-                        .map(CharSequence::toString)
-                        .toArray(String[]::new));
-    }
-
-    private int getAcceptedSuggestionIndex() {
-        String newLabel =
-                checkNotNull(mFolderName.getText().toString(),
-                        "Expected valid folder label, but found null");
-
-        return getSuggestedLabels()
-                .map(suggestionsArray ->
-                        IntStream.range(0, suggestionsArray.length)
-                                .filter(index -> newLabel.equalsIgnoreCase(
-                                        suggestionsArray[index]))
-                                .findFirst()
-                                .orElse(-1)
-                ).orElse(-1);
-
-    }
-
-
-    private Target.Builder newEditTextTargetBuilder() {
-        return Target.newBuilder().setType(Target.Type.ITEM).setItemType(ItemType.EDITTEXT);
-    }
-
-    private Target.Builder newFolderTargetBuilder() {
-        return Target.newBuilder()
-                .setType(Target.Type.CONTAINER)
-                .setContainerType(ContainerType.FOLDER)
-                .setPageIndex(mInfo.screenId)
-                .setGridX(mInfo.cellX)
-                .setGridY(mInfo.cellY)
-                .setCardinality(mInfo.contents.size());
-    }
-
-    private Target.Builder newParentContainerTarget() {
-        Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
-
-        switch (mInfo.container) {
-            case CONTAINER_HOTSEAT:
-                return builder.setContainerType(ContainerType.HOTSEAT);
-            case CONTAINER_DESKTOP:
-                return builder.setContainerType(ContainerType.WORKSPACE);
-            default:
-                throw new AssertionError(String
-                        .format("Expected container to be either %s or %s but found %s.",
-                                CONTAINER_HOTSEAT,
-                                CONTAINER_DESKTOP,
-                                mInfo.container));
-        }
+    /**
+     * 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/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 eda9545..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;
@@ -38,22 +42,19 @@
 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.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;
@@ -62,6 +63,16 @@
 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;
@@ -73,15 +84,16 @@
 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,
-        DraggableView {
+        DraggableView, Reorderable {
 
     @Thunk ActivityContext mActivity;
     @Thunk Folder mFolder;
-    private FolderInfo mInfo;
+    public FolderInfo mInfo;
 
     private CheckLongPressHelper mLongPressHelper;
 
@@ -117,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
@@ -189,8 +205,7 @@
         icon.mActivity = activity;
         icon.mDotRenderer = grid.mDotRendererWorkSpace;
 
-        icon.setContentDescription(
-                group.getContext().getString(R.string.folder_name_format, folderInfo.title));
+        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();
@@ -224,16 +239,6 @@
         mBackground.getBounds(outBounds);
     }
 
-    @Override
-    public int getViewType() {
-        return DRAGGABLE_ICON;
-    }
-
-    @Override
-    public void getVisualDragBounds(Rect bounds) {
-        getPreviewBounds(bounds);
-    }
-
     public float getBackgroundStrokeWidth() {
         return mBackground.getStrokeWidth();
     }
@@ -387,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,
@@ -397,16 +410,15 @@
             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
             final int finalIndex = index;
 
-            FolderNameInfo[] nameInfos =
-                    new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+            FolderNameInfos nameInfos = new FolderNameInfos();
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                Executors.UI_HELPER_EXECUTOR.post(() -> {
+                Executors.MODEL_EXECUTOR.post(() -> {
                     d.folderNameProvider.getSuggestedFolderName(
                             getContext(), mInfo.contents, nameInfos);
-                    showFinalView(finalIndex, item, nameInfos);
+                    showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
                 });
             } else {
-                showFinalView(finalIndex, item, nameInfos);
+                showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
             }
         } else {
             addItem(item);
@@ -414,15 +426,60 @@
     }
 
     private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
-            FolderNameInfo[] nameInfos) {
+            FolderNameInfos nameInfos, InstanceId instanceId) {
         postDelayed(() -> {
             mPreviewItemManager.hidePreviewItem(finalIndex, false);
             mFolder.showItem(item);
+            setLabelSuggestion(nameInfos, instanceId);
             invalidate();
-            mFolder.showSuggestedTitle(nameInfos);
         }, 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) {
@@ -631,6 +688,7 @@
         mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
         boolean isDotted = mDotInfo.hasDot();
         updateDotScale(wasDotted, isDotted);
+        setContentDescription(getAccessiblityTitle(mInfo.title));
         invalidate();
         requestLayout();
     }
@@ -641,13 +699,14 @@
         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
@@ -692,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
index edf2c70..6038a05 100644
--- a/src/com/android/launcher3/folder/FolderNameEditText.java
+++ b/src/com/android/launcher3/folder/FolderNameEditText.java
@@ -70,8 +70,11 @@
         for (int i = 0; i < cnt; i++) {
             cInfo[i] = new CompletionInfo(i, i, suggestList.get(i));
         }
-        post(() -> getContext().getSystemService(InputMethodManager.class)
-                .displayCompletions(this, cInfo));
+        // 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 */);
     }
 
     /**
diff --git a/src/com/android/launcher3/folder/FolderNameInfo.java b/src/com/android/launcher3/folder/FolderNameInfo.java
deleted file mode 100644
index 1287219..0000000
--- a/src/com/android/launcher3/folder/FolderNameInfo.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.folder;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-
-/**
- * Information about a single label suggestions of the Folder.
- */
-
-public final class FolderNameInfo implements Parcelable {
-    private final double mScore;
-    private final CharSequence mLabel;
-
-    /**
-     * Create a simple completion with label.
-     *
-     * @param label The text that should be inserted into the editor and pushed to
-     *              InputMethodManager suggestions.
-     * @param score The score for the label between 0.0 and 1.0.
-     */
-    public FolderNameInfo(CharSequence label, double score) {
-        mScore = score;
-        mLabel = label;
-    }
-
-    private FolderNameInfo(Parcel source) {
-        mScore = source.readDouble();
-        mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
-    }
-
-    public CharSequence getLabel() {
-        return mLabel;
-    }
-
-    /**
-     * Used to package this object into a {@link Parcel}.
-     *
-     * @param dest  The {@link Parcel} to be written.
-     * @param flags The flags used for parceling.
-     */
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeDouble(mScore);
-        TextUtils.writeToParcel(mLabel, dest, flags);
-    }
-
-    /**
-     * Used to make this class parcelable.
-     */
-    @NonNull
-    public static final Parcelable.Creator<FolderNameInfo> CREATOR =
-            new Parcelable.Creator<FolderNameInfo>() {
-                public FolderNameInfo createFromParcel(Parcel source) {
-                    return new FolderNameInfo(source);
-                }
-
-                public FolderNameInfo[] newArray(int size) {
-                    return new FolderNameInfo[size];
-                }
-            };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    @NonNull
-    public String toString() {
-        return String.format("%s:%.2f", mLabel, mScore);
-    }
-}
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 07161da..d166e27 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -22,15 +22,16 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
@@ -64,6 +65,7 @@
     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;
@@ -71,6 +73,7 @@
 
     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);
@@ -93,10 +96,10 @@
      */
     public void getSuggestedFolderName(Context context,
             ArrayList<WorkspaceItemInfo> workspaceItemInfos,
-            FolderNameInfo[] nameInfos) {
-
+            FolderNameInfos nameInfos) {
+        Preconditions.assertWorkerThread();
         if (DEBUG) {
-            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos));
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
         }
         // If all the icons are from work profile,
         // Then, suggest "Work" as the folder name
@@ -121,7 +124,7 @@
             info.ifPresent(i -> setAsFirstSuggestion(nameInfos, i.title.toString()));
         }
         if (DEBUG) {
-            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(nameInfos));
+            Log.d(TAG, "getSuggestedFolderName:" + nameInfos.toString());
         }
     }
 
@@ -135,39 +138,37 @@
                 .findAny();
     }
 
-    private void setAsFirstSuggestion(FolderNameInfo[] nameInfos, CharSequence label) {
-        if (nameInfos.length == 0 || contains(nameInfos, label)) {
+    private void setAsFirstSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
             return;
         }
-        for (int i = nameInfos.length - 1; i > 0; i--) {
-            if (nameInfos[i - 1] != null && !TextUtils.isEmpty(nameInfos[i - 1].getLabel())) {
-                nameInfos[i] = nameInfos[i - 1];
+        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]);
             }
         }
-        nameInfos[0] = new FolderNameInfo(label, 1.0);
+        nameInfos.setLabel(0, label, 1.0f);
     }
 
-    private void setAsLastSuggestion(FolderNameInfo[] nameInfos, CharSequence label) {
-        if (nameInfos.length == 0 || contains(nameInfos, label)) {
+    private void setAsLastSuggestion(FolderNameInfos nameInfos, CharSequence label) {
+        if (nameInfos == null || nameInfos.contains(label)) {
             return;
         }
-
-        for (int i = 0; i < nameInfos.length; i++) {
-            if (nameInfos[i] == null || TextUtils.isEmpty(nameInfos[i].getLabel())) {
-                nameInfos[i] = new FolderNameInfo(label, 1.0);
+        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.
-        int lastIndex = nameInfos.length - 1;
-        nameInfos[lastIndex] = new FolderNameInfo(label, 1.0);
-    }
-
-    private boolean contains(FolderNameInfo[] nameInfos, CharSequence label) {
-        return Arrays.stream(nameInfos)
-                .filter(Objects::nonNull)
-                .anyMatch(nameInfo -> nameInfo.getLabel().toString().equalsIgnoreCase(
-                        label.toString()));
+        nameInfos.setLabel(labels.length - 1, label, 1.0f);
     }
 
     private class FolderNameWorker extends BaseModelUpdateTask {
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index dcd0e14..32531c0 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.PagedView;
@@ -44,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;
@@ -153,6 +153,7 @@
             CellLayout page = (CellLayout) getChildAt(i);
             ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
             for (int j = container.getChildCount() - 1; j >= 0; j--) {
+                container.getChildAt(j).setVisibility(View.VISIBLE);
                 mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
             }
             page.removeAllViews();
diff --git a/src/com/android/launcher3/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 27aa43e..7f8a15c 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -38,8 +38,8 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index ed9dfbb..21822a3 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.config.FeatureFlags;
 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 java.nio.ByteBuffer;
@@ -76,11 +77,12 @@
 
         if (mView instanceof DraggableView) {
             DraggableView dv = (DraggableView) mView;
-            dv.prepareDrawDragView();
-            dv.getVisualDragBounds(mTempRect);
-            destCanvas.translate(blurSizeOutline / 2 - mTempRect.left,
-                    blurSizeOutline / 2 - mTempRect.top);
-            mView.draw(destCanvas);
+            try (SafeCloseable t = dv.prepareDrawDragView()) {
+                dv.getSourceVisualDragBounds(mTempRect);
+                destCanvas.translate(blurSizeOutline / 2 - mTempRect.left,
+                        blurSizeOutline / 2 - mTempRect.top);
+                mView.draw(destCanvas);
+            }
         }
         destCanvas.restoreToCount(saveCount);
     }
@@ -92,8 +94,10 @@
     public Bitmap createDragBitmap() {
         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).getVisualDragBounds(mTempRect);
+            ((DraggableView) mView).getSourceVisualDragBounds(mTempRect);
             width = mTempRect.width();
             height = mTempRect.height();
         } else {
@@ -102,7 +106,7 @@
         }
 
         return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
-                height + blurSizeOutline, (c) -> drawDragView(c, 1));
+                height + blurSizeOutline, (c) -> drawDragView(c, scale));
     }
 
     public final void generateDragOutline(Bitmap preview) {
diff --git a/src/com/android/launcher3/graphics/GridOptionsProvider.java b/src/com/android/launcher3/graphics/GridOptionsProvider.java
index 607aba9..08d7e4c 100644
--- a/src/com/android/launcher3/graphics/GridOptionsProvider.java
+++ b/src/com/android/launcher3/graphics/GridOptionsProvider.java
@@ -1,36 +1,28 @@
 package com.android.launcher3.graphics;
 
-import static com.android.launcher3.config.FeatureFlags.USE_SURFACE_VIEW_FOR_GRID_PREVIEW;
-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;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
 import com.android.launcher3.R;
-import com.android.launcher3.uioverrides.PreviewSurfaceRenderer;
 
 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.
@@ -61,26 +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";
-
     private static final String METHOD_GET_PREVIEW = "get_preview";
-    private static final String METADATA_KEY_PREVIEW_VERSION = "preview_version";
-
-
-
-    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);
-                    }
-                }
-            };
 
     @Override
     public boolean onCreate() {
@@ -105,10 +78,6 @@
                     .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
                             && idp.numRows == gridOption.numRows);
         }
-        Bundle metadata = new Bundle();
-        metadata.putString(METADATA_KEY_PREVIEW_VERSION,
-                USE_SURFACE_VIEW_FOR_GRID_PREVIEW.get() ? "V2" : "V1");
-        cursor.setExtras(metadata);
         return cursor;
     }
 
@@ -133,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";
     }
 
@@ -174,39 +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());
-        }
-
-        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());
-        }
-    }
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras)  {
         if (!METHOD_GET_PREVIEW.equals(method)) {
             return null;
         }
 
-        PreviewSurfaceRenderer.render(getContext(), extras);
-        return null;
+        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 7d4eb0e..885fb66 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -20,8 +20,8 @@
 import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
-import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
 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;
 
@@ -33,8 +33,6 @@
 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;
@@ -54,39 +52,38 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.FolderInfo;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 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.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
 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.GridSizeMigrationTask;
-import com.android.launcher3.model.GridSizeMigrationTaskV2;
 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;
@@ -101,7 +98,6 @@
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.FutureTask;
@@ -117,7 +113,7 @@
  *   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";
 
@@ -209,15 +205,17 @@
     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();
@@ -239,28 +237,6 @@
                 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);
@@ -376,6 +352,13 @@
             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;
@@ -396,20 +379,9 @@
 
         private void populate() {
             if (ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get()) {
-                boolean needsToMigrate =
-                        MULTI_DB_GRID_MIRATION_ALGO.get()
-                                ? GridSizeMigrationTaskV2.needsToMigrate(mContext, mIdp)
-                                : GridSizeMigrationTask.needsToMigrate(mContext, mIdp);
-                boolean success = false;
-                if (needsToMigrate) {
-                    success = MULTI_DB_GRID_MIRATION_ALGO.get()
-                            ? GridSizeMigrationTaskV2.migrateGridIfNeeded(mContext, mIdp)
-                            : GridSizeMigrationTask.migrateGridIfNeeded(mContext, mIdp);
-                }
-
                 WorkspaceFetcher fetcher;
                 PreviewContext previewContext = null;
-                if (needsToMigrate && success) {
+                if (mMigrated) {
                     previewContext = new PreviewContext(mContext, mIdp);
                     LauncherAppState appForPreview = new LauncherAppState(
                             previewContext, null /* iconCacheFileName */);
@@ -468,6 +440,21 @@
                             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++) {
@@ -509,12 +496,6 @@
             // Additional measure for views which use auto text size API
             measureView(mRootView, mDp.widthPx, mDp.heightPx);
         }
-
-        private void renderScreenShot(Canvas canvas) {
-            populate();
-            mRootView.draw(canvas);
-            dispatchVisibilityAggregated(mRootView, false);
-        }
     }
 
     private static void measureView(View view, int width, int height) {
@@ -561,7 +542,7 @@
             }
 
             return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
-                    mBgDataModel.widgetsModel);
+                    mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
         }
     }
 
@@ -590,7 +571,7 @@
             loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI);
             mBgDataModel.widgetsModel.update(mApp, null);
             return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
-                    mBgDataModel.widgetsModel);
+                    mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
         }
     }
 
@@ -610,12 +591,15 @@
     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, WidgetsModel widgetsModel) {
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                ArrayList<AppInfo> cachedPredictedItems, WidgetsModel widgetsModel) {
             mWorkspaceItems = workspaceItems;
             mAppWidgets = appWidgets;
+            mCachedPredictedItems = cachedPredictedItems;
             mWidgetsModel = widgetsModel;
         }
     }
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index b0e1db1..e85b056 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -31,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;
 
@@ -73,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();
@@ -81,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;
@@ -105,7 +107,7 @@
     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
         super(info.bitmap);
         mItem = info;
-        mProgressPath = getShapePath();
+        mShapePath = getShapePath();
         mScaledTrackPath = new Path();
         mScaledProgressPath = new Path();
 
@@ -127,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);
 
@@ -141,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;
         }
@@ -155,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;
     }
 
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/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index f27c9da..ff0f773 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -38,18 +38,18 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.AppInfo;
 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.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;
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/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index bfeb1dc..6bc1ecb 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -235,6 +235,9 @@
      * 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);
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 1b70fde..cd4f034 100644
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ b/src/com/android/launcher3/logging/LoggerUtils.java
@@ -19,10 +19,10 @@
 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.ItemType;
@@ -41,6 +41,7 @@
     private static final ArrayMap<Class, SparseArray<String>> sNameCache = new ArrayMap<>();
     private static final String UNKNOWN = "UNKNOWN";
     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;
@@ -173,4 +174,17 @@
         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 2829951..8e23b65 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -15,62 +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 androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.logger.LauncherAtom.ItemInfo;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
+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;
 
 /**
  * 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 {
 
-    interface EventEnum {
+    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 {
-        @LauncherUiEvent(doc = "App launched from workspace, hotseat or folder in launcher")
-        APP_LAUNCH_TAP(1),
-        @LauncherUiEvent(doc = "Task launched from overview using TAP")
-        TASK_LAUNCH_TAP(2),
-        @LauncherUiEvent(doc = "Task launched from overview using SWIPE DOWN")
-        TASK_LAUNCH_SWIPE_DOWN(2),
-        @LauncherUiEvent(doc = "TASK dismissed from overview using SWIPE UP")
-        TASK_DISMISS_SWIPE_UP(3);
+        /* 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;
         }
     }
 
-    protected LogStateProvider mStateProvider;
+    /**
+     * Launcher specific ranking related events.
+     */
+    public enum LauncherRankingEvent implements EventEnum {
 
-    public static StatsLogManager newInstance(Context context, LogStateProvider stateProvider) {
+        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;
     }
 
     /**
-     * Logs an event and accompanying {@link ItemInfo}
+     * 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(LauncherEvent eventId, LauncherAtom.ItemInfo itemInfo) { }
+    public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName,
+            int position) {
+    }
 
     /**
      * Logs snapshot, or impression of the current workspace.
      */
-    public void logSnapshot() { }
-
-    public void verify() {}     // TODO: should move into robo tests
+    public void logSnapshot() {
+    }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogUtils.java b/src/com/android/launcher3/logging/StatsLogUtils.java
index 8449612..a5cc7ea 100644
--- a/src/com/android/launcher3/logging/StatsLogUtils.java
+++ b/src/com/android/launcher3/logging/StatsLogUtils.java
@@ -1,38 +1,20 @@
 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 androidx.annotation.Nullable;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+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 {
 
@@ -64,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/LauncherUiEvent.java b/src/com/android/launcher3/logging/UiEvent.java
similarity index 80%
rename from src/com/android/launcher3/logging/LauncherUiEvent.java
rename to src/com/android/launcher3/logging/UiEvent.java
index 4507ff7..20d6c72 100644
--- a/src/com/android/launcher3/logging/LauncherUiEvent.java
+++ b/src/com/android/launcher3/logging/UiEvent.java
@@ -13,6 +13,7 @@
  * 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;
@@ -23,8 +24,11 @@
 
 @Retention(SOURCE)
 @Target(FIELD)
-public @interface LauncherUiEvent {
-    /** An explanation, suitable for Android analysts, of the UI event that this log represents. */
+//  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 89077ee..e094cab 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -46,11 +46,10 @@
 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.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;
@@ -143,12 +142,6 @@
             fillIntentInfo(itemTarget, intent, userHandle);
         }
         LauncherEvent event = newLauncherEvent(action,  targets);
-        ItemInfo info = v == null ? null : (ItemInfo) v.getTag();
-        if (info != null && Utilities.IS_DEBUG_DEVICE && FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
-            FileLog.d(TAG, "appLaunch: packageName:" + info.getTargetComponent().getPackageName()
-                    + ",isWorkApp:" + (info.user != null && !Process.myUserHandle().equals(
-                    userHandle)) + ",launchLocation:" + info.container);
-        }
         dispatchUserEvent(event, intent);
         mAppOrTaskLaunch = true;
     }
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index eb95395..c236fa6 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -23,16 +23,16 @@
 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.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 9f1843f..eb5d106 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -16,8 +16,8 @@
 
 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;
@@ -33,10 +33,11 @@
 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.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;
@@ -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.
      *
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 0d12183..8b0ef7b 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -17,18 +17,19 @@
 package com.android.launcher3.model;
 
 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.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.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.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
@@ -95,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();
@@ -196,6 +198,10 @@
             // 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).
@@ -247,6 +253,11 @@
             }
         }
 
+        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) {
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 5a7b4d3..9013cba 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -17,13 +17,13 @@
 
 import android.util.Log;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+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;
@@ -117,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 206688a..9bef847 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -26,16 +26,16 @@
 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.Workspace;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
+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;
@@ -93,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.
@@ -342,6 +349,13 @@
     }
 
     public interface Callbacks {
+        // 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;
+
         /**
          * Returns the page number to bind first, synchronously if possible or -1
          */
@@ -365,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 4a1bc4d..acfc339 100644
--- a/src/com/android/launcher3/model/GridBackupTable.java
+++ b/src/com/android/launcher3/model/GridBackupTable.java
@@ -128,6 +128,32 @@
     }
 
     /**
+     * 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());
+        copyTable(mDb, Favorites.TABLE_NAME, tableName, profileId);
+        encodeDBProperties(0);
+    }
+
+    /**
+     *
+     * 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) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index a084600..e8a52bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -1,5 +1,7 @@
 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;
@@ -22,7 +24,6 @@
 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;
@@ -32,6 +33,7 @@
 import com.android.launcher3.Workspace;
 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;
@@ -53,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;
@@ -894,8 +893,7 @@
         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,
-                idp.numHotseatIcons);
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
     }
 
     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 79ae4c5..ebdfa8c 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -16,6 +16,8 @@
 
 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;
 
@@ -30,18 +32,18 @@
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
-import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 
 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;
 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;
@@ -50,12 +52,13 @@
 
 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;
-import java.util.stream.Collectors;
 
 /**
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
@@ -63,9 +66,6 @@
  */
 public class GridSizeMigrationTaskV2 {
 
-    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 TAG = "GridSizeMigrationTaskV2";
     private static final boolean DEBUG = true;
 
@@ -110,8 +110,7 @@
         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,
-                idp.numHotseatIcons);
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
     }
 
     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
@@ -148,14 +147,6 @@
 
         SharedPreferences prefs = Utilities.getPrefs(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.
-            return true;
-        }
-
         HashSet<String> validPackages = getValidPackages(context);
         int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
 
@@ -236,9 +227,8 @@
             if (DEBUG) {
                 Log.d(TAG, "Migrating " + screenId);
             }
-            List<DbEntry> entries = mDestReader.loadWorkspaceEntries(screenId);
             GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
-                    mDestReader, mContext, entries, screenId, mTrgX, mTrgY, mWorkspaceDiff);
+                    mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff);
             workspaceSolution.find();
             if (mWorkspaceDiff.isEmpty()) {
                 break;
@@ -248,8 +238,7 @@
         int screenId = mDestReader.mLastScreenId + 1;
         while (!mWorkspaceDiff.isEmpty()) {
             GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
-                    mDestReader, mContext, new ArrayList<>(), screenId, mTrgX, mTrgY,
-                    mWorkspaceDiff);
+                    mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff);
             workspaceSolution.find();
             screenId++;
         }
@@ -258,75 +247,88 @@
 
     /** Return what's in the src but not in the dest */
     private static List<DbEntry> calcDiff(List<DbEntry> src, List<DbEntry> dest) {
-        Set<String> destSet = dest.parallelStream().map(DbEntry::getIntentStr).collect(
-                Collectors.toSet());
+        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 (!destSet.contains(entry.mIntent)) {
-                diff.add(entry);
+            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 void insertEntryInDb(SQLiteDatabase db, Context context,
-            ArrayList<DbEntry> entriesFromSrcDb, DbEntry entry, String srcTableName,
-            String destTableName) {
-        int id = -1;
-        switch (entry.itemType) {
-            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
-                for (DbEntry e : entriesFromSrcDb) {
-                    if (TextUtils.equals(e.mIntent, entry.mIntent)) {
-                        id = e.id;
-                    }
-                }
-
-                break;
-            }
-            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
-                for (DbEntry e : entriesFromSrcDb) {
-                    if (e.mFolderItems.size() == entry.mFolderItems.size()
-                            && e.mFolderItems.containsAll(entry.mFolderItems)) {
-                        id = e.id;
-                    }
-                }
-                break;
-            }
-            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-            case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
-                for (DbEntry e : entriesFromSrcDb) {
-                    if (TextUtils.equals(e.mProvider, entry.mProvider)) {
-                        id = e.id;
-                        break;
-                    }
-                }
-                break;
-            }
-            default:
-                return;
+    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;
+    }
 
-        Cursor c = db.query(srcTableName, null, LauncherSettings.Favorites._ID + " = '" + id + "'",
+    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);
-            entry.updateContentValues(values);
-            values.put(LauncherSettings.Favorites._ID,
-                    LauncherSettings.Settings.call(context.getContentResolver(),
-                            LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
-                            LauncherSettings.Settings.EXTRA_VALUE));
+            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 entryId) {
+    private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
         db.delete(tableName,
-                Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryId), null);
+                Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null);
     }
 
     private static HashSet<String> getValidPackages(Context context) {
@@ -362,8 +364,7 @@
         private int mNextStartY;
 
         GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader,
-                Context context, List<DbEntry> placedWorkspaceItems, int screenId, int trgX,
-                int trgY, List<DbEntry> itemsToPlace) {
+                Context context, int screenId, int trgX, int trgY, List<DbEntry> itemsToPlace) {
             mDb = db;
             mSrcReader = srcReader;
             mDestReader = destReader;
@@ -374,8 +375,11 @@
             mTrgY = trgY;
             mNextStartX = 0;
             mNextStartY = mTrgY - 1;
-            for (DbEntry entry : placedWorkspaceItems) {
-                mOccupied.markCells(entry, true);
+            List<DbEntry> existedEntries = mDestReader.mWorkspaceEntriesByScreenId.get(screenId);
+            if (existedEntries != null) {
+                for (DbEntry entry : existedEntries) {
+                    mOccupied.markCells(entry, true);
+                }
             }
             mItemsToPlace = itemsToPlace;
         }
@@ -389,15 +393,20 @@
                     continue;
                 }
                 if (findPlacement(entry)) {
-                    insertEntryInDb(mDb, mContext, mSrcReader.mWorkspaceEntries, entry,
-                            mSrcReader.mTableName, mDestReader.mTableName);
+                    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 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,
@@ -416,6 +425,7 @@
                         return true;
                     }
                 }
+                mNextStartX = 0;
             }
             return false;
         }
@@ -453,8 +463,8 @@
                     // to something other than -1.
                     entry.cellX = i;
                     entry.cellY = 0;
-                    insertEntryInDb(mDb, mContext, mSrcReader.mHotseatEntries, entry,
-                            mSrcReader.mTableName, mDestReader.mTableName);
+                    insertEntryInDb(mDb, mContext, entry, mSrcReader.mTableName,
+                            mDestReader.mTableName);
                     mOccupied.markCells(entry, true);
                 }
             }
@@ -485,6 +495,8 @@
 
         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) {
@@ -574,25 +586,6 @@
             return loadWorkspaceEntries(c);
         }
 
-        protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
-            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
-                            + " AND " + LauncherSettings.Favorites.SCREEN + " = " + screen);
-            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);
@@ -670,6 +663,10 @@
                     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();
@@ -684,10 +681,14 @@
             int total = 0;
             while (c.moveToNext()) {
                 try {
+                    int id = c.getInt(0);
                     String intent = c.getString(1);
                     verifyIntent(intent);
                     total++;
-                    entry.mFolderItems.add(intent);
+                    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)));
                 }
@@ -726,7 +727,7 @@
 
         private String mIntent;
         private String mProvider;
-        private Set<String> mFolderItems = new HashSet<>();
+        private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
 
         /** Comparator according to the reading order */
         @Override
@@ -760,9 +761,5 @@
             values.put(LauncherSettings.Favorites.SPANX, spanX);
             values.put(LauncherSettings.Favorites.SPANY, spanY);
         }
-
-        public String getIntentStr() {
-            return mIntent;
-        }
     }
 }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 695d2a6..165d1ea 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -37,19 +37,19 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.AppInfo;
 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.WorkspaceItemInfo;
 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;
@@ -65,7 +65,7 @@
 
     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;
@@ -100,9 +100,11 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app) {
+    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
+            UserManagerState userManagerState) {
         super(cursor);
 
+        allUsers = userManagerState.allUsers;
         mContentUri = contentUri;
         mContext = app.getContext();
         mIconCache = app.getIconCache();
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index fc0997b..102ec31 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -16,11 +16,14 @@
 
 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.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;
@@ -35,6 +38,7 @@
 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.net.Uri;
 import android.os.UserHandle;
@@ -45,20 +49,17 @@
 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.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.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
-import com.android.launcher3.folder.FolderNameInfo;
+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;
@@ -67,6 +68,12 @@
 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;
@@ -117,6 +124,8 @@
     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,
@@ -178,6 +187,7 @@
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
             loadWorkspace(allShortcuts);
+            loadCachedPredictions();
             logger.addSplit("loadWorkspace");
 
             verifyNotStopped();
@@ -329,7 +339,8 @@
 
             Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();
             final LoaderCursor c = new LoaderCursor(
-                    contentResolver.query(contentUri, null, null, null, null), contentUri, mApp);
+                    contentResolver.query(contentUri, null, null, null, null), contentUri, mApp,
+                    mUserManagerState);
 
             Map<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
 
@@ -348,12 +359,13 @@
                         LauncherSettings.Favorites.OPTIONS);
 
                 final LongSparseArray<UserHandle> allUsers = c.allUsers;
-                final LongSparseArray<Boolean> quietMode = new LongSparseArray<>();
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
+
+                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);
 
@@ -400,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();
 
@@ -534,7 +546,7 @@
                                             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.
@@ -848,6 +860,24 @@
         }
     }
 
+    @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 = mUserCache.getUserProfiles();
         List<LauncherActivityInfo> allActivityList = new ArrayList<>();
@@ -861,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);
@@ -879,6 +909,22 @@
                         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;
@@ -887,8 +933,8 @@
     private List<ShortcutInfo> loadDeepShortcuts() {
         List<ShortcutInfo> allShortcuts = new ArrayList<>();
         mBgDataModel.deepShortcutMap.clear();
-        mBgDataModel.hasShortcutHostPermission = hasShortcutsPermission(mApp.getContext());
-        if (mBgDataModel.hasShortcutHostPermission) {
+
+        if (mBgAllAppsList.hasShortcutHostPermission()) {
             for (UserHandle user : mUserCache.getUserProfiles()) {
                 if (mUserManager.isUserUnlocked(user)) {
                     List<ShortcutInfo> shortcuts = new ShortcutRequest(mApp.getContext(), user)
@@ -907,13 +953,12 @@
 
         synchronized (mBgDataModel) {
             for (int i = 0; i < mBgDataModel.folders.size(); i++) {
-                FolderNameInfo[] suggestionInfos =
-                        new FolderNameInfo[FolderNameProvider.SUGGEST_MAX];
+                FolderNameInfos suggestionInfos = new FolderNameInfos();
                 FolderInfo info = mBgDataModel.folders.valueAt(i);
                 if (info.suggestedFolderNames == null) {
                     provider.getSuggestedFolderName(mApp.getContext(), info.contents,
                             suggestionInfos);
-                    info.suggestedFolderNames = new Intent().putExtra("suggest", suggestionInfos);
+                    info.suggestedFolderNames = suggestionInfos;
                 }
             }
         }
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index 1473124..4efeba5 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -16,14 +16,17 @@
 package com.android.launcher3.model;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 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}.
@@ -109,4 +112,17 @@
             }
         });
     }
+
+    /**
+     * 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 27fa580..2c99df7 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -27,20 +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.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;
 
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 2832150..203f1ca 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -19,13 +19,13 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
-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.PromiseAppInfo;
-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.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;
 
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 48c56e9..7cd467e 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,8 +15,9 @@
  */
 package com.android.launcher3.model;
 
-import static com.android.launcher3.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
-import static com.android.launcher3.WorkspaceItemInfo.FLAG_RESTORED_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;
@@ -29,20 +30,20 @@
 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.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.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.testing.TestProtocol;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -86,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();
 
@@ -154,14 +151,21 @@
                 if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
                 appsList.updateDisabledFlags(matcher, flagOp);
                 break;
-            case OP_USER_AVAILABILITY_CHANGE:
-                flagOp = context.getSystemService(UserManager.class).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();
@@ -233,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;
diff --git a/src/com/android/launcher3/model/PagedViewOrientedState.java b/src/com/android/launcher3/model/PagedViewOrientedState.java
deleted file mode 100644
index e48b8c1..0000000
--- a/src/com/android/launcher3/model/PagedViewOrientedState.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- *
- *  * Copyright (C) 2020 The Android Open Source Project
- *  *
- *  * Licensed under the Apache License, Version 2.0 (the "License");
- *  * you may not use this file except in compliance with the License.
- *  * You may obtain a copy of the License at
- *  *
- *  *      http://www.apache.org/licenses/LICENSE-2.0
- *  *
- *  * Unless required by applicable law or agreed to in writing, software
- *  * distributed under the License is distributed on an "AS IS" BASIS,
- *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  * See the License for the specific language governing permissions and
- *  * limitations under the License.
- *
- */
-
-package com.android.launcher3.model;
-
-import android.view.Surface;
-
-import com.android.launcher3.states.RotationHelper;
-import com.android.launcher3.touch.PortraitPagedViewHandler;
-import com.android.launcher3.touch.LandscapePagedViewHandler;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.SeascapePagedViewHandler;
-
-/**
- * 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.
- *
- * Currently this class resides in {@link com.android.launcher3.PagedView}, but there's a ticket
- * to disassociate it from Launcher since it's needed before Launcher is instantiated
- * See TODO(b/150300347)
- */
-public final class PagedViewOrientedState {
-
-    private PagedOrientationHandler mOrientationHandler = new PortraitPagedViewHandler();
-
-    private int mTouchRotation = Surface.ROTATION_0;
-    private int mDisplayRotation = Surface.ROTATION_0;
-    /**
-     * If {@code true} we default to {@link PortraitPagedViewHandler} and don't support any fake
-     * launcher orientations.
-     */
-    private boolean mDisableMultipleOrientations;
-
-    /**
-     * 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
-     */
-    public void update(int touchRotation, int displayRotation) {
-        if (mDisableMultipleOrientations) {
-            return;
-        }
-
-        mDisplayRotation = displayRotation;
-        mTouchRotation = touchRotation;
-        if (mTouchRotation == Surface.ROTATION_90) {
-            mOrientationHandler = new LandscapePagedViewHandler();
-        } else if (mTouchRotation == Surface.ROTATION_270) {
-            mOrientationHandler = new SeascapePagedViewHandler();
-        } else {
-            mOrientationHandler = new PortraitPagedViewHandler();
-        }
-    }
-
-    public boolean areMultipleLayoutOrientationsDisabled() {
-        return mDisableMultipleOrientations;
-    }
-
-    /**
-     * Setting this preference will render future calls to {@link #update(int, int)} as a no-op.
-     */
-    public void disableMultipleOrientations(boolean disable) {
-        mDisableMultipleOrientations = disable;
-        if (disable) {
-            mOrientationHandler = new PortraitPagedViewHandler();
-        }
-    }
-
-    public int getDisplayRotation() {
-        return mDisplayRotation;
-    }
-
-    /**
-     * Gets the difference between the rotation of the device/display and which region the
-     * user is currently interacting with in factors of 90 degree clockwise rotations.
-     * Ex. Display is in portrait -> 0, user touches landscape region -> 1, this
-     * method would return 3 because it takes 3 clockwise 90 degree rotations from normal to
-     * landscape (portrait -> seascape -> reverse portrait -> landscape)
-     */
-    public int getTouchDisplayDelta() {
-        return RotationHelper.deltaRotation(mTouchRotation, mDisplayRotation);
-    }
-
-    public PagedOrientationHandler getOrientationHandler() {
-        return mOrientationHandler;
-    }
-}
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/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 3f79ad0..1cbe5c2 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -19,10 +19,10 @@
 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.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;
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index a3adc82..7ec884f 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -15,17 +15,16 @@
  */
 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 android.os.UserManager;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.WorkspaceItemInfo;
+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;
@@ -43,18 +42,19 @@
 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 = context.getSystemService(UserManager.class).isUserUnlocked(mUser);
 
         HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
-        if (isUserUnlocked) {
+        if (mIsUserUnlocked) {
             QueryResult shortcuts = new ShortcutRequest(context, mUser)
                     .query(ShortcutRequest.PINNED);
             if (shortcuts.wasSuccess()) {
@@ -65,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;
             }
         }
 
@@ -77,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
@@ -108,7 +108,7 @@
             }
         }
 
-        if (isUserUnlocked) {
+        if (mIsUserUnlocked) {
             dataModel.updateDeepShortcutCounts(
                     null, mUser,
                     new ShortcutRequest(context, mUser).query(ShortcutRequest.ALL));
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/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
similarity index 92%
rename from src/com/android/launcher3/AppInfo.java
rename to src/com/android/launcher3/model/data/AppInfo.java
index f76ca50..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;
@@ -28,6 +30,8 @@
 
 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;
 
@@ -72,7 +76,7 @@
 
     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);
 
@@ -121,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 93%
rename from src/com/android/launcher3/ItemInfoWithIcon.java
rename to src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 1941455..d95f94f 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
 
 import com.android.launcher3.icons.BitmapInfo;
 
@@ -60,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.
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
similarity index 91%
rename from src/com/android/launcher3/LauncherAppWidgetInfo.java
rename to src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index 3a478dd..b0d19a6 100644
--- a/src/com/android/launcher3/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -14,15 +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 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.model.PackageItemInfo;
 import com.android.launcher3.util.ContentWriter;
 
 /**
@@ -140,6 +144,12 @@
         return appWidgetId <= CUSTOM_WIDGET_ID;
     }
 
+    @Nullable
+    @Override
+    public ComponentName getTargetComponent() {
+        return providerName;
+    }
+
     @Override
     public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
@@ -154,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;
@@ -169,8 +179,8 @@
     }
 
     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) {
diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/data/PackageItemInfo.java
similarity index 95%
rename from src/com/android/launcher3/model/PackageItemInfo.java
rename to src/com/android/launcher3/model/data/PackageItemInfo.java
index 2fc064c..b70d0d4 100644
--- a/src/com/android/launcher3/model/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/data/PackageItemInfo.java
@@ -14,9 +14,8 @@
  * 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;
diff --git a/src/com/android/launcher3/PromiseAppInfo.java b/src/com/android/launcher3/model/data/PromiseAppInfo.java
similarity index 97%
rename from src/com/android/launcher3/PromiseAppInfo.java
rename to src/com/android/launcher3/model/data/PromiseAppInfo.java
index e55e4bd..b6231ed 100644
--- a/src/com/android/launcher3/PromiseAppInfo.java
+++ b/src/com/android/launcher3/model/data/PromiseAppInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model.data;
 
 import android.content.Context;
 import android.content.Intent;
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
similarity index 94%
rename from src/com/android/launcher3/WorkspaceItemInfo.java
rename to src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index be907e5..a7bf1f3 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,7 +25,9 @@
 
 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.ApiWrapper;
@@ -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());
     }
 
     /**
@@ -199,8 +201,8 @@
 
     /** 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 dummy componentName instead of adding additional
             // check everywhere.
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/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b193ffd..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;
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 976d7ba..901d27f 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -56,8 +56,6 @@
     // 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 String KEY_INSTALL_SESSION_CREATED_TIMESTAMP =
-            "key_install_session_created_timestamp";
 
     private static final boolean DEBUG = false;
 
@@ -166,14 +164,13 @@
     }
 
     /**
-     * Attempt to restore workspace layout if the session is triggered due to device restore and it
-     * has a newer timestamp.
+     * 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) && hasNewerTimestamp(mAppContext, info)) {
+        if (isRestore(info)) {
             LauncherSettings.Settings.call(mAppContext.getContentResolver(),
                     LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE);
             return true;
@@ -186,13 +183,6 @@
         return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
     }
 
-    private static boolean hasNewerTimestamp(
-            @NonNull final Context context, @NonNull final SessionInfo info) {
-        return PackageManagerHelper.getSessionCreatedTimeInMillis(info)
-                > Utilities.getDevicePrefs(context).getLong(
-                        KEY_INSTALL_SESSION_CREATED_TIMESTAMP, 0);
-    }
-
     public boolean promiseIconAddedForId(int sessionId) {
         return mPromiseIconIds.contains(sessionId);
     }
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 74a5a31..179061f 100644
--- a/src/com/android/launcher3/pm/PinRequestHelper.java
+++ b/src/com/android/launcher3/pm/PinRequestHelper.java
@@ -30,8 +30,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.ShortcutCachingLogic;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 public class PinRequestHelper {
 
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index ac0e065..40d7031 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -40,9 +40,9 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index f723256..5aab41a 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
@@ -107,22 +106,6 @@
     }
 
     /**
-     * Returns true if any user profile has quiet mode enabled.
-     */
-    public boolean isAnyProfileQuietModeEnabled() {
-        List<UserHandle> userProfiles = getUserProfiles();
-        for (UserHandle userProfile : userProfiles) {
-            if (Process.myUserHandle().equals(userProfile)) {
-                continue;
-            }
-            if (mUserManager.isQuietModeEnabled(userProfile)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
      * @see UserManager#getSerialNumberForUser(UserHandle)
      */
     public long getSerialNumberForUser(UserHandle user) {
@@ -160,16 +143,4 @@
         List<UserHandle> users = mUserManager.getUserProfiles();
         return users == null ? Collections.emptyList() : users;
     }
-
-    /**
-     * Returns true is there is at least one user profile enabled
-     */
-    public boolean hasWorkProfile() {
-        synchronized (this) {
-            if (mUsers != null) {
-                return mUsers.size() > 1;
-            }
-        }
-        return getUserProfiles().size() > 1;
-    }
 }
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 18bc55a..d5b32fc 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -293,8 +293,8 @@
             // gravity to CENTER_HORIZONTAL, but continue below to update y.
         } else {
             boolean canBeLeftAligned = x + width + insets.left
-                    < dragLayer.getRight() - insets.right;
-            boolean canBeRightAligned = x > dragLayer.getLeft() + insets.left;
+                    < dragLayer.getWidth() - insets.right;
+            boolean canBeRightAligned = x > insets.left;
             boolean alignmentStillValid = mIsLeftAligned && canBeLeftAligned
                     || !mIsLeftAligned && canBeRightAligned;
             if (!alignmentStillValid) {
@@ -367,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;
         }
@@ -387,6 +389,11 @@
         return Pair.create(this, "");
     }
 
+    @Override
+    protected View getAccessibilityInitialFocusView() {
+        return getChildCount() > 0 ? getChildAt(0) : this;
+    }
+
     private void animateOpen() {
         setVisibility(View.VISIBLE);
 
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 406e1b2..614cf14 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -16,6 +16,7 @@
 
 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;
@@ -49,10 +50,9 @@
 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;
@@ -60,6 +60,9 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 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;
@@ -397,6 +400,9 @@
         } 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);
@@ -671,8 +677,10 @@
             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, sv.getFinalInfo(),
+                    mContainer, itemInfo,
                     new ShortcutDragPreviewProvider(sv.getIconView(), iconShift),
                     new DragOptions());
             dv.animateShift(-iconShift.x, -iconShift.y);
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 1092c7b..5a5f668 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -23,9 +23,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.ItemInfo;
 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.util.ComponentKey;
@@ -33,6 +33,7 @@
 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;
@@ -238,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 fdcf04f..5ed6f2e 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -16,6 +16,8 @@
 
 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;
@@ -25,10 +27,10 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.WorkspaceItemInfo;
 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.notification.NotificationListener;
@@ -142,7 +144,7 @@
                     infos = Collections.emptyList();
                 } else {
                     infos = notificationListener.getNotificationsForKeys(notificationKeys).stream()
-                            .map(sbn -> new NotificationInfo(launcher, sbn))
+                            .map(sbn -> new NotificationInfo(launcher, sbn, originalInfo))
                             .collect(Collectors.toList());
                 }
                 uiHandler.post(() -> container.applyNotificationInfos(infos));
@@ -160,6 +162,7 @@
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, launcher);
                 cache.getUnbadgedShortcutIcon(si, shortcut);
                 si.rank = i;
+                si.container = CONTAINER_SHORTCUTS;
 
                 final DeepShortcutView view = shortcutViews.get(i);
                 uiHandler.post(() -> view.applyShortcutInfo(si, shortcut, container));
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 8751202..61829c0 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,6 +16,7 @@
 
 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;
@@ -33,9 +34,9 @@
 
 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)
@@ -76,6 +77,8 @@
     @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();
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 21c5ac5..fd292a3 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,5 +1,7 @@
 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;
@@ -14,11 +16,11 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 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.ControlType;
 import com.android.launcher3.util.InstantAppResolver;
@@ -27,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.
@@ -103,7 +106,6 @@
     };
 
     public static class Widgets extends SystemShortcut<Launcher> {
-
         public Widgets(Launcher target, ItemInfo itemInfo) {
             super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo);
         }
@@ -117,6 +119,8 @@
             widgetsBottomSheet.populateAndShow(mItemInfo);
             mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
                     ControlType.WIDGETS_BUTTON, view);
+            mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
+                    .log(LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP);
         }
     }
 
@@ -137,6 +141,8 @@
                     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);
         }
     }
 
@@ -145,8 +151,9 @@
                 && ((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;
+        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;
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
index 732fb0b..a5462a6 100644
--- a/src/com/android/launcher3/provider/ImportDataTask.java
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -37,7 +37,6 @@
 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;
@@ -46,6 +45,7 @@
 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;
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 0fe3673..53183bf 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.provider;
 
-import static com.android.launcher3.pm.InstallSessionHelper.KEY_INSTALL_SESSION_CREATED_TIMESTAMP;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.app.backup.BackupManager;
@@ -35,13 +34,13 @@
 import com.android.launcher3.AppWidgetsRestoredReceiver;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
@@ -87,13 +86,10 @@
      */
     public static boolean restoreIfPossible(@NonNull Context context,
             @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) {
-        Utilities.getDevicePrefs(context).edit().putLong(
-                KEY_INSTALL_SESSION_CREATED_TIMESTAMP, System.currentTimeMillis()).apply();
         final SQLiteDatabase db = helper.getWritableDatabase();
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
             task.restoreWorkspace(context, db, helper, backupManager);
-            task.restoreAppWidgetIdsIfExists(context);
             t.commit();
             return true;
         } catch (Exception e) {
@@ -107,7 +103,6 @@
      */
     private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
         InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-        // TODO(pinyaoting): Support backing up workspace with multiple grid options.
         new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
                 .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
     }
@@ -115,13 +110,17 @@
     private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db,
             @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
             throws Exception {
-        // TODO(pinyaoting): Support restoring workspace with multiple grid options.
         final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
         GridBackupTable backupTable = new GridBackupTable(context, db, idp.numHotseatIcons,
                 idp.numColumns, idp.numRows);
         if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
-            sanitizeDB(helper, db, backupManager);
+            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);
+            }
         }
     }
 
@@ -132,8 +131,10 @@
      *      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();
@@ -210,6 +211,7 @@
         if (myProfileId != oldProfileId) {
             changeDefaultColumn(db, myProfileId);
         }
+        return itemsDeleted;
     }
 
     /**
diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
index 54b7fb9..e9058c3 100644
--- a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
+++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java
@@ -30,12 +30,12 @@
 import android.widget.BaseAdapter;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.ItemInfo;
 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;
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 1cc01f4..21ad275 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -28,18 +28,18 @@
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseDraggingActivity;
 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;
-import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 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;
@@ -89,17 +89,16 @@
         if (mDragLayer != null) {
             return;
         }
-        InvariantDeviceProfile mainIdp = LauncherAppState.getIDP(this);
         InvariantDeviceProfile currentDisplayIdp =
                 new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay());
 
-        // Pick the device profile with the smaller icon size so that the cached icons are
-        // shown properly
-        if (mainIdp.iconBitmapSize <= currentDisplayIdp.iconBitmapSize) {
-            mDeviceProfile = mainIdp.getDeviceProfile(this).copy(this);
-        } else {
-            mDeviceProfile = currentDisplayIdp.getDeviceProfile(this);
-        }
+        // 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);
@@ -197,6 +196,9 @@
     public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
 
     @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
+    @Override
     public void bindScreens(IntArray orderedScreenIds) { }
 
     @Override
@@ -294,8 +296,8 @@
     }
 
     @Override
-    public void bindAllApplications(AppInfo[] apps) {
-        mAppsView.getAppsStore().setApps(apps);
+    public void bindAllApplications(AppInfo[] apps, int flags) {
+        mAppsView.getAppsStore().setApps(apps, flags);
     }
 
     public PopupDataProvider getPopupDataProvider() {
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 936d377..40630d3 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -30,9 +30,9 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
 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;
@@ -117,10 +117,12 @@
             if (child == mAppsView) {
                 int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx
                         + grid.cellLayoutPaddingLeftRightPx);
-                int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
 
+                int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
                 int appsWidth = Math.min(width, maxWidth);
-                int appsHeight = Math.round(appsWidth * (float) height / (float) width);
+
+                int maxHeight = grid.allAppsCellHeightPx * idp.numAllAppsColumns + padding;
+                int appsHeight = Math.min(height, maxHeight);
 
                 mAppsView.measure(
                         makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY));
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index 8dc2e61..b12d04f 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -216,17 +216,45 @@
         if (launchSandboxIntent.resolveActivity(context.getPackageManager()) == null) {
             return;
         }
-        PreferenceCategory sandboxCategory = newCategory("Sandbox");
-        Preference launchSandboxPreference = new Preference(context);
-        launchSandboxPreference.setKey("launchSandbox");
-        launchSandboxPreference.setTitle("Launch Gesture Navigation Sandbox");
-        launchSandboxPreference.setSummary(
-                "This provides tutorials and a place to practice navigation gestures.");
-        launchSandboxPreference.setOnPreferenceClickListener(preference -> {
-            startActivity(launchSandboxIntent);
+        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(launchSandboxPreference);
+        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) {
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 12085c8..d3213a1 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -23,10 +23,7 @@
 import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
-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;
@@ -48,7 +45,6 @@
 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;
 
@@ -71,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);
@@ -95,26 +89,7 @@
     }
 
     @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 && getSupportFragmentManager().isStateSaved()) {
@@ -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 9cc7d8f..e9b92e2 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 
 /**
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index fa1a85f..3ca9490 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -6,7 +6,7 @@
 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;
 
 /**
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/LauncherStateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
similarity index 67%
rename from src/com/android/launcher3/LauncherStateManager.java
rename to src/com/android/launcher3/statemanager/StateManager.java
index e071777..60b87d9 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.statemanager;
 
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
 
 import android.animation.Animator;
@@ -25,93 +24,62 @@
 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.compat.AccessibilityManagerCompat;
 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;
 
 /**
- * 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
+ * Class to manage transitions between different states for a StatefulActivity based on different
+ * states
  */
-public class LauncherStateManager {
+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 Launcher mLauncher;
-    private final ArrayList<StateListener> mListeners = new ArrayList<>();
+    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 Animator[] mStateElementAnimators;
+    private final AtomicAnimationFactory mAtomicAnimationFactory;
 
-    private StateHandler[] mStateHandlers;
-    private LauncherState mState = NORMAL;
+    private StateHandler<STATE_TYPE>[] mStateHandlers;
+    private STATE_TYPE mState;
 
-    private LauncherState mLastStableState = NORMAL;
-    private LauncherState mCurrentStableState = NORMAL;
+    private STATE_TYPE mLastStableState;
+    private STATE_TYPE mCurrentStableState;
 
-    private LauncherState mRestState;
+    private STATE_TYPE mRestState;
 
-    public LauncherStateManager(Launcher l) {
+    public StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState) {
         mUiHandler = new Handler(Looper.getMainLooper());
-        mLauncher = l;
+        mActivity = l;
+        mBaseState = baseState;
+        mState = mLastStableState = mCurrentStableState = baseState;
+        mAtomicAnimationFactory = l.createAtomicAnimationFactory();
     }
 
-    public LauncherState getState() {
+    public STATE_TYPE getState() {
         return mState;
     }
 
-    public LauncherState getCurrentStableState() {
+    public STATE_TYPE getCurrentStableState() {
         return mCurrentStableState;
     }
 
     public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "LauncherState:");
+        writer.println(prefix + "StateManager:");
         writer.println(prefix + "\tmLastStableState:" + mLastStableState);
         writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
         writer.println(prefix + "\tmState:" + mState);
@@ -121,7 +89,7 @@
 
     public StateHandler[] getStateHandlers() {
         if (mStateHandlers == null) {
-            mStateHandlers = mLauncher.createStateHandlers();
+            mStateHandlers = mActivity.createStateHandlers();
         }
         return mStateHandlers;
     }
@@ -138,29 +106,29 @@
      * Returns true if the state changes should be animated.
      */
     public boolean shouldAnimateStateChange() {
-        return !mLauncher.isForceInvisible() && mLauncher.isStarted();
+        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(LauncherState state) {
+    public boolean isInStableState(STATE_TYPE state) {
         return mState == state && mCurrentStableState == state
                 && (mConfig.targetState == null || mConfig.targetState == state);
     }
 
     /**
-     * @see #goToState(LauncherState, boolean, Runnable)
+     * @see #goToState(STATE_TYPE, boolean, Runnable)
      */
-    public void goToState(LauncherState state) {
+    public void goToState(STATE_TYPE state) {
         goToState(state, shouldAnimateStateChange());
     }
 
     /**
-     * @see #goToState(LauncherState, boolean, Runnable)
+     * @see #goToState(STATE_TYPE, boolean, Runnable)
      */
-    public void goToState(LauncherState state, boolean animated) {
+    public void goToState(STATE_TYPE state, boolean animated) {
         goToState(state, animated, 0, null);
     }
 
@@ -171,21 +139,21 @@
      *                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) {
+    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(LauncherState state, long delay, Runnable onCompleteRunnable) {
+    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(LauncherState state, long delay) {
+    public void goToState(STATE_TYPE state, long delay) {
         goToState(state, true, delay, null);
     }
 
@@ -196,7 +164,7 @@
     public void reapplyState(boolean cancelCurrentAnimation) {
         boolean wasInAnimation = mConfig.currentAnimation != null;
         if (cancelCurrentAnimation) {
-            cancelAllStateElementAnimation();
+            mAtomicAnimationFactory.cancelAllStateElementAnimation();
             cancelAnimation();
         }
         if (mConfig.currentAnimation == null) {
@@ -209,10 +177,10 @@
         }
     }
 
-    private void goToState(LauncherState state, boolean animated, long delay,
+    private void goToState(STATE_TYPE state, boolean animated, long delay,
             final Runnable onCompleteRunnable) {
-        animated &= Utilities.areAnimationsEnabled(mLauncher);
-        if (mLauncher.isInState(state)) {
+        animated &= Utilities.areAnimationsEnabled(mActivity);
+        if (mActivity.isInState(state)) {
             if (mConfig.currentAnimation == null) {
                 // Run any queued runnable
                 if (onCompleteRunnable != null) {
@@ -230,11 +198,11 @@
         }
 
         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
-        LauncherState fromState = mState;
+        STATE_TYPE fromState = mState;
         mConfig.reset();
 
         if (!animated) {
-            cancelAllStateElementAnimation();
+            mAtomicAnimationFactory.cancelAllStateElementAnimation();
             onStateTransitionStart(state);
             for (StateHandler handler : getStateHandlers()) {
                 handler.setState(state);
@@ -263,15 +231,15 @@
         }
     }
 
-    private void goToStateAnimated(LauncherState state, LauncherState fromState,
+    private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
             Runnable onCompleteRunnable) {
-        // Since state NORMAL can be reached from multiple states, just assume that the
+        // 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 == NORMAL
-                ? fromState.getTransitionDuration(mLauncher)
-                : state.getTransitionDuration(mLauncher);
+        mConfig.duration = state == mBaseState
+                ? fromState.getTransitionDuration(mActivity)
+                : state.getTransitionDuration(mActivity);
         prepareForAtomicAnimation(fromState, state, mConfig);
-        AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).getAnim();
+        AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
         if (onCompleteRunnable != null) {
             animation.addListener(AnimationSuccessListener.forRunnable(onCompleteRunnable));
         }
@@ -283,46 +251,46 @@
      * - 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,
+    public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
             StateAnimationConfig config) {
-        toState.prepareForAtomicAnimation(mLauncher, fromState, config);
+        mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
     }
 
     /**
      * Creates an animation representing atomic transitions between the provided states
      */
     public AnimatorSet createAtomicAnimation(
-            LauncherState fromState, LauncherState toState, StateAnimationConfig config) {
+            STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
         PendingAnimation builder = new PendingAnimation(config.duration);
         prepareForAtomicAnimation(fromState, toState, config);
 
-        for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
+        for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
             handler.setStateWithAnimation(toState, config, builder);
         }
-        return builder.getAnim();
+        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 normal playback. Use higher duration for better
+     * @param duration intended duration for state playback. Use higher duration for better
      *                accuracy.
      */
     public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState state, long duration) {
+            STATE_TYPE state, long duration) {
         return createAnimationToNewWorkspace(state, duration, ANIM_ALL_COMPONENTS);
     }
 
     public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState state, long duration, @AnimationFlags int animComponents) {
+            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(LauncherState state,
+    public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
             StateAnimationConfig config) {
         config.userControlled = true;
         mConfig.reset();
@@ -332,12 +300,18 @@
         return mConfig.playbackController;
     }
 
-    private PendingAnimation createAnimationToNewWorkspaceInternal(final LauncherState state) {
-        PendingAnimation builder = new PendingAnimation(mConfig.duration);
-        for (StateHandler handler : getStateHandlers()) {
-            handler.setStateWithAnimation(state, mConfig, builder);
+    private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "createAnimationToNewWorkspaceInternal: "
+                    + state);
         }
-        builder.getAnim().addListener(new AnimationSuccessListener() {
+        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) {
@@ -347,53 +321,43 @@
 
             @Override
             public void onAnimationSuccess(Animator animator) {
+                if (TestProtocol.sDebugTracing) {
+                    Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
+                }
                 onStateTransitionEnd(state);
             }
         });
-        mConfig.setAnimation(builder.getAnim(), state);
+        mConfig.setAnimation(builder.buildAnim(), state);
         return builder;
     }
 
-    private void onStateTransitionStart(LauncherState state) {
-        if (mState != state) {
-            mState.onStateDisabled(mLauncher);
-        }
+    private void onStateTransitionStart(STATE_TYPE state) {
         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);
-        }
+        mActivity.onStateSetStart(mState);
 
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionStart(state);
         }
     }
 
-    private void onStateTransitionEnd(LauncherState 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;
         }
 
-        state.onStateTransitionEnd(mLauncher);
-        mLauncher.onStateSetEnd(state);
-
-        if (state == NORMAL) {
+        mActivity.onStateSetEnd(state);
+        if (state == mBaseState) {
             setRestState(null);
         }
 
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionComplete(state);
         }
-
-        AccessibilityManagerCompat.sendStateEventToTest(mLauncher, state.ordinal);
     }
 
-    public LauncherState getLastState() {
+    public STATE_TYPE getLastState() {
         return mLastStableState;
     }
 
@@ -402,18 +366,18 @@
             // The user is doing something. Lets not mess it up
             return;
         }
-        if (mState.disableRestore) {
+        if (mState.shouldDisableRestore()) {
             goToState(getRestState());
             // Reset history
-            mLastStableState = NORMAL;
+            mLastStableState = mBaseState;
         }
     }
 
-    public LauncherState getRestState() {
-        return mRestState == null ? NORMAL : mRestState;
+    public STATE_TYPE getRestState() {
+        return mRestState == null ? mBaseState : mRestState;
     }
 
-    public void setRestState(LauncherState restState) {
+    public void setRestState(STATE_TYPE restState) {
         mRestState = restState;
     }
 
@@ -461,42 +425,23 @@
         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();
+        if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) {
+            mAtomicAnimationFactory.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;
+        Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values);
+        mAtomicAnimationFactory.mStateElementAnimators[index] = anim;
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                mStateElementAnimators[index] = null;
+                mAtomicAnimationFactory.mStateElementAnimators[index] = null;
             }
         });
         return anim;
@@ -527,13 +472,14 @@
         }
     }
 
-    private static class AnimationState extends StateAnimationConfig implements AnimatorListener {
+    private static class AnimationState<STATE_TYPE> extends StateAnimationConfig
+            implements AnimatorListener {
 
         private static final StateAnimationConfig DEFAULT = new StateAnimationConfig();
 
         public AnimatorPlaybackController playbackController;
         public AnimatorSet currentAnimation;
-        public LauncherState targetState;
+        public STATE_TYPE targetState;
 
         // Id to keep track of config changes, to tie an animation with the corresponding request
         public int changeId = 0;
@@ -568,7 +514,7 @@
             }
         }
 
-        public void setAnimation(AnimatorSet animation, LauncherState targetState) {
+        public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) {
             currentAnimation = animation;
             this.targetState = targetState;
             currentAnimation.addListener(this);
@@ -584,23 +530,68 @@
         public void onAnimationRepeat(Animator animator) { }
     }
 
-    public interface StateHandler {
+    public interface StateHandler<STATE_TYPE> {
 
         /**
          * Updates the UI to {@param state} without any animations
          */
-        void setState(LauncherState state);
+        void setState(STATE_TYPE state);
 
         /**
          * Sets the UI to {@param state} by animating any changes.
          */
         void setStateWithAnimation(
-                LauncherState toState, StateAnimationConfig config, PendingAnimation animation);
+                STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation);
     }
 
-    public interface StateListener {
+    public interface StateListener<STATE_TYPE> {
 
-        void onStateTransitionStart(LauncherState toState);
-        void onStateTransitionComplete(LauncherState finalState);
+        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
index 43f30f1..b8a184f 100644
--- a/src/com/android/launcher3/states/HintState.java
+++ b/src/com/android/launcher3/states/HintState.java
@@ -15,10 +15,10 @@
  */
 package com.android.launcher3.states;
 
+import android.content.Context;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**
@@ -26,7 +26,7 @@
  */
 public class HintState extends LauncherState {
 
-    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_DISABLE_RESTORE
+    private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE | FLAG_DISABLE_RESTORE
             | FLAG_HAS_SYS_UI_SCRIM;
 
     public HintState(int id) {
@@ -34,13 +34,23 @@
     }
 
     @Override
-    public int getTransitionDuration(Launcher launcher) {
+    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.9f, 0, 0);
+        return new ScaleAndTranslation(0.92f, 0, 0);
     }
 
     @Override
@@ -48,16 +58,4 @@
         // Treat the QSB as part of the hotseat so they move together.
         return getHotseatScaleAndTranslation(launcher);
     }
-
-    @Override
-    public void onStateTransitionEnd(Launcher launcher) {
-        LauncherStateManager stateManager = launcher.getStateManager();
-        Workspace workspace = launcher.getWorkspace();
-        boolean willMoveScreens = workspace.getNextPage() != Workspace.DEFAULT_PAGE;
-        stateManager.goToState(NORMAL, true, willMoveScreens ? null
-                : launcher.getScrimView()::startDragHandleEducationAnim);
-        if (willMoveScreens) {
-            workspace.post(workspace::moveToDefaultScreen);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 8bb6a08..5a60f53 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -15,52 +15,43 @@
  */
 package com.android.launcher3.states;
 
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 
-import static com.android.launcher3.config.FeatureFlags.FLAG_ENABLE_FIXED_ROTATION_TRANSFORM;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.res.Resources;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
+import android.database.ContentObserver;
+import android.os.Handler;
 import android.provider.Settings;
-import android.view.MotionEvent;
-import android.view.Surface;
+import android.util.Log;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.UiThreadHelper;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * Utility class to manage launcher rotation
  */
 public class RotationHelper implements OnSharedPreferenceChangeListener {
 
+    private static final String TAG = "RotationHelper";
+
     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
 
-    public static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
     private final ContentResolver mContentResolver;
+    private boolean mSystemAutoRotateEnabled;
 
-    /**
-     * Listener to receive changes when {@link #FIXED_ROTATION_TRANSFORM_SETTING_NAME} flag changes.
-     */
-    public interface ForcedRotationChangedListener {
-        void onForcedRotationChanged(boolean isForcedRotation);
-    }
+    private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateAutoRotateSetting();
+        }
+    };
 
     public static boolean getAllowRotationDefaultValue() {
         // If the device's pixel density was scaled (usually via settings for A11y), use the
@@ -77,12 +68,9 @@
 
     private final Activity mActivity;
     private final SharedPreferences mSharedPrefs;
-    private final SharedPreferences mFeatureFlagsPrefs;
 
     private boolean mIgnoreAutoRotateSettings;
-    private boolean mAutoRotateEnabled;
-    private boolean mForcedRotation;
-    private List<ForcedRotationChangedListener> mForcedRotationChangedListeners = new ArrayList<>();
+    private boolean mHomeRotationEnabled;
 
     /**
      * Rotation request made by
@@ -113,67 +101,35 @@
         if (!mIgnoreAutoRotateSettings) {
             mSharedPrefs = Utilities.getPrefs(mActivity);
             mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
-            mAutoRotateEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+            mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                     getAllowRotationDefaultValue());
         } else {
             mSharedPrefs = null;
         }
 
         mContentResolver = activity.getContentResolver();
-        mFeatureFlagsPrefs = Utilities.getFeatureFlagsPrefs(mActivity);
-        mFeatureFlagsPrefs.registerOnSharedPreferenceChangeListener(this);
-        updateForcedRotation(true);
     }
 
-    /**
-     * @param setValueFromPrefs If true, then {@link #mForcedRotation} will get set to the value
-     *                          from the home developer settings. Otherwise it will not.
-     *                          This is primarily to allow tests to set their own conditions.
-     */
-    private void updateForcedRotation(boolean setValueFromPrefs) {
-        boolean isForcedRotation = mFeatureFlagsPrefs
-                .getBoolean(FLAG_ENABLE_FIXED_ROTATION_TRANSFORM, true)
-                && !getAllowRotationDefaultValue();
-        if (mForcedRotation == isForcedRotation) {
-            return;
+    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);
         }
-        if (setValueFromPrefs) {
-            mForcedRotation = isForcedRotation;
-        }
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (mActivity.checkSelfPermission(WRITE_SECURE_SETTINGS) == PERMISSION_GRANTED) {
-                Settings.Global.putInt(mContentResolver, FIXED_ROTATION_TRANSFORM_SETTING_NAME,
-                            mForcedRotation ? 1 : 0);
-            }
-        });
-        for (ForcedRotationChangedListener listener : mForcedRotationChangedListeners) {
-            listener.onForcedRotationChanged(mForcedRotation);
-        }
-    }
 
-    /**
-     * will not be called when first registering the listener.
-     */
-    public void addForcedRotationCallback(ForcedRotationChangedListener listener) {
-        mForcedRotationChangedListeners.add(listener);
-    }
-
-    public void removeForcedRotationCallback(ForcedRotationChangedListener listener) {
-        mForcedRotationChangedListeners.remove(listener);
+        mSystemAutoRotateEnabled = autoRotateEnabled == 1;
     }
 
     @Override
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
-        if (FLAG_ENABLE_FIXED_ROTATION_TRANSFORM.equals(s)) {
-            updateForcedRotation(true);
-            return;
-        }
-
-        boolean wasRotationEnabled = mAutoRotateEnabled;
-        mAutoRotateEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+        boolean wasRotationEnabled = mHomeRotationEnabled;
+        mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                 getAllowRotationDefaultValue());
-        if (mAutoRotateEnabled != wasRotationEnabled) {
+        if (mHomeRotationEnabled != wasRotationEnabled) {
             notifyChange();
+            updateAutoRotateSetting();
         }
     }
 
@@ -202,10 +158,6 @@
     public void forceAllowRotationForTesting(boolean allowRotation) {
         mIgnoreAutoRotateSettings =
                 allowRotation || mActivity.getResources().getBoolean(R.bool.allow_rotation);
-        // TODO(b/150214193) Tests currently expect launcher to be able to be rotated
-        //   Modify tests for this new behavior
-        mForcedRotation = !allowRotation;
-        updateForcedRotation(false);
         notifyChange();
     }
 
@@ -213,6 +165,11 @@
         if (!mInitialized) {
             mInitialized = true;
             notifyChange();
+
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
+                    false, mSystemAutoRotateObserver);
+            updateAutoRotateSetting();
         }
     }
 
@@ -222,8 +179,7 @@
             if (mSharedPrefs != null) {
                 mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
             }
-            mForcedRotationChangedListeners.clear();
-            mFeatureFlagsPrefs.unregisterOnSharedPreferenceChangeListener(this);
+            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
         }
     }
 
@@ -233,10 +189,7 @@
         }
 
         final int activityFlags;
-        if (mForcedRotation) {
-            // TODO(b/150214193) Properly address this
-            activityFlags = SCREEN_ORIENTATION_PORTRAIT;
-        } else if (mStateHandlerRequest != REQUEST_NONE) {
+        if (mStateHandlerRequest != REQUEST_NONE) {
             activityFlags = mStateHandlerRequest == REQUEST_LOCK ?
                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
         } else if (mCurrentTransitionRequest != REQUEST_NONE) {
@@ -245,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
@@ -258,39 +211,6 @@
         }
     }
 
-    public static int getDegreesFromRotation(int rotation) {
-        int degrees;
-        switch (rotation) {
-            case Surface.ROTATION_90:
-                degrees = 90;
-                break;
-            case Surface.ROTATION_180:
-                degrees = 180;
-                break;
-            case Surface.ROTATION_270:
-                degrees = 270;
-                break;
-            case Surface.ROTATION_0:
-            default:
-                degrees = 0;
-                break;
-        }
-        return degrees;
-    }
-
-    public static int getRotationFromDegrees(float degrees) {
-        int threshold = 70;
-        if (degrees >= (360 - threshold) || degrees < (threshold)) {
-            return Surface.ROTATION_0;
-        } else if (degrees < (90 + threshold)) {
-            return Surface.ROTATION_270;
-        } else if (degrees < 180 + threshold) {
-            return Surface.ROTATION_180;
-        } else {
-            return Surface.ROTATION_90;
-        }
-    }
-
     /**
      * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
      * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
@@ -302,99 +222,12 @@
         return delta;
     }
 
-    /**
-     * 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 static void transformEventForNavBar(MotionEvent ev, boolean inverse) {
-        // TODO(b/151269990): Use a temp matrix
-        Matrix m = new Matrix();
-        m.setScale(1, -1);
-        if (inverse) {
-            Matrix inv = new Matrix();
-            m.invert(inv);
-            ev.transform(inv);
-        } else {
-            ev.transform(m);
-        }
-    }
-
-    /**
-     * Creates a matrix to transform the given motion event specified by degrees.
-     * If {@param inverse} is {@code true}, the inverse of that matrix will be applied
-     */
-    public static void transformEvent(float degrees, MotionEvent ev, boolean inverse) {
-        Matrix transform = new Matrix();
-        // TODO(b/151269990): Use a temp matrix
-        transform.setRotate(degrees);
-        if (inverse) {
-            Matrix inv = new Matrix();
-            transform.invert(inv);
-            ev.transform(inv);
-        } else {
-            ev.transform(transform);
-        }
-        // TODO: Add scaling back in based on degrees
-//        if (getWidth() > 0 && getHeight() > 0) {
-//            float scale = ((float) getWidth()) / getHeight();
-//            transform.postScale(scale, 1 / scale);
-//        }
-    }
-
-    /**
-     * TODO(b/149658423): Have {@link com.android.quickstep.OrientationTouchTransformer
-     *   also use this}
-     */
-    public static Matrix getRotationMatrix(int screenWidth, int screenHeight, int displayRotation) {
-        Matrix m = new Matrix();
-        // TODO(b/151269990): Use a temp matrix
-        switch (displayRotation) {
-            case Surface.ROTATION_0:
-                return m;
-            case Surface.ROTATION_90:
-                m.setRotate(360 - RotationHelper.getDegreesFromRotation(displayRotation));
-                m.postTranslate(0, screenWidth);
-                break;
-            case Surface.ROTATION_270:
-                m.setRotate(360 - RotationHelper.getDegreesFromRotation(displayRotation));
-                m.postTranslate(screenHeight, 0);
-                break;
-        }
-        return m;
-    }
-
-    public static void mapRectFromNormalOrientation(RectF src, int screenWidth, int screenHeight,
-        int displayRotation) {
-        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
-        m.mapRect(src);
-    }
-
-    public static void mapInverseRectFromNormalOrientation(RectF src, int screenWidth,
-        int screenHeight, int displayRotation) {
-        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
-        Matrix inverse = new Matrix();
-        m.invert(inverse);
-        inverse.mapRect(src);
-    }
-
-    public static void getTargetRectForRotation(Rect srcOut, int screenWidth, int screenHeight,
-        int displayRotation) {
-        RectF wrapped = new RectF(srcOut);
-        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
-        m.mapRect(wrapped);
-        wrapped.round(srcOut);
-    }
-
-    public static boolean isRotationLandscape(int rotation) {
-        return rotation == Surface.ROTATION_270 || rotation == Surface.ROTATION_90;
-    }
-
     @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 2ba624c..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.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,16 +29,17 @@
  */
 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, STATE_FLAGS);
+        super(id, ContainerType.WORKSPACE, STATE_FLAGS);
     }
 
     @Override
-    public int getTransitionDuration(Launcher launcher) {
+    public int getTransitionDuration(Context context) {
         return 150;
     }
 
@@ -78,7 +76,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 0.5f;
     }
 
@@ -88,28 +86,7 @@
     }
 
     @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
index 82cde64..f90ad3c 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -37,6 +37,7 @@
             PLAY_ATOMIC_OVERVIEW_SCALE,
             PLAY_ATOMIC_OVERVIEW_PEEK,
             SKIP_OVERVIEW,
+            SKIP_DEPTH_CONTROLLER
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimationFlags {}
@@ -44,6 +45,7 @@
     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;
@@ -67,6 +69,8 @@
             ANIM_ALL_APPS_FADE,
             ANIM_OVERVIEW_SCRIM_FADE,
             ANIM_ALL_APPS_HEADER_FADE,
+            ANIM_OVERVIEW_MODAL,
+            ANIM_DEPTH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -83,8 +87,10 @@
     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 = 13;
+    private static final int ANIM_TYPES_COUNT = 15;
 
     private final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index e786f07..38b3712 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -15,27 +15,17 @@
  */
 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.system.Os;
-import android.util.Log;
-import android.view.View;
 import android.view.WindowInsets;
 
-import androidx.annotation.Keep;
-
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
@@ -44,10 +34,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
 
-import java.util.LinkedList;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -65,7 +52,6 @@
     protected Context mContext;
     protected DeviceProfile mDeviceProfile;
     protected LauncherAppState mLauncherAppState;
-    private static LinkedList mLeaks;
 
     public void init(Context context) {
         mContext = context;
@@ -77,15 +63,6 @@
     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_ALL_APPS_SWIPE_HEIGHT: {
                 return getLauncherUIProperty(Bundle::putInt, l -> {
                     final float progress = LauncherState.NORMAL.getVerticalProgress(l)
@@ -99,14 +76,6 @@
                 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:
                 return getLauncherUIProperty(Bundle::putBoolean, l -> {
                     l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
@@ -118,11 +87,6 @@
                     return true;
                 });
 
-            case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
-                return getLauncherUIProperty(Bundle::putInt,
-                        l -> l.getAppsView().getAppsStore().getDeferUpdatesFlags());
-            }
-
             case TestProtocol.REQUEST_APPS_LIST_SCROLL_Y: {
                 return getLauncherUIProperty(Bundle::putInt,
                         l -> l.getAppsView().getActiveRecyclerView().getCurrentScrollY());
@@ -137,59 +101,19 @@
                 }, this::getCurrentActivity);
             }
 
-            case TestProtocol.REQUEST_PID: {
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
-                break;
-            }
-
-            case TestProtocol.REQUEST_TOTAL_PSS_KB: {
-                runGcAndFinalizersSync();
-                Debug.MemoryInfo mem = new Debug.MemoryInfo();
-                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_VIEW_LEAK: {
-                if (mLeaks == null) mLeaks = new LinkedList();
-                mLeaks.add(new View(mContext));
-                break;
-            }
-
             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;
-                break;
+                return response;
+
+            default:
+                return null;
         }
-        return response;
     }
 
     protected boolean isLauncherInitialized() {
@@ -201,22 +125,6 @@
         return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
     }
 
-    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);
-        }
-    }
-
     /**
      * Returns the result by getting a Launcher property on UI thread
      */
@@ -257,21 +165,4 @@
          */
         void set(Bundle b, String key, T value);
     }
-
-    // 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();
-                }
-            }
-        };
-    }
 }
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
index d522d81..51e0819 100644
--- a/src/com/android/launcher3/testing/TestLogging.java
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -21,9 +21,17 @@
 
 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) {
@@ -43,4 +51,8 @@
             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 3181752..3ca08fd 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -28,10 +28,11 @@
     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 HINT_STATE_ORDINAL = 7;
+    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";
@@ -47,6 +48,8 @@
                 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:
@@ -56,7 +59,7 @@
             case HINT_STATE_ORDINAL:
                 return "Hint";
             default:
-                return null;
+                return "Unknown";
         }
     }
 
@@ -85,6 +88,10 @@
     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";
@@ -96,9 +103,7 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-
-    public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
-    public static final String APP_NOT_DISABLED = "b/139891609";
-    public static final String NO_SCROLL_END_WIDGETS = "b/152354290";
-    public static final String NO_START_FROM_RECENTS = "b/152658211";
+    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 cbc5257..3c78b08 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -21,6 +21,8 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 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;
@@ -31,6 +33,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
@@ -41,11 +44,14 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 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.userevent.nano.LauncherLogProto;
+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.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);
     }
 
@@ -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;
@@ -233,6 +246,7 @@
     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.
@@ -542,10 +563,22 @@
         // 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() {
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 1276ece..01b33d8 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -26,6 +26,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.testing.TestProtocol;
+
 import java.util.LinkedList;
 import java.util.Queue;
 
@@ -173,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);
                 }
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index f7ce160..8486666 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -15,14 +15,15 @@
  */
 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.Intent;
@@ -38,20 +39,23 @@
 
 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.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;
@@ -109,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);
         }
     }
 
@@ -224,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));
@@ -239,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 8537bdf..7baeab8 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -28,11 +28,11 @@
 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;
 
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 6715bc1..48c7734 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -16,6 +16,7 @@
 
 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;
@@ -30,9 +31,8 @@
 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.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -65,16 +65,21 @@
     }
 
     @Override
-    public void getCurveProperties(PagedView view, Rect mInsets, CurveProperties out) {
+    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 = mInsets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
+        out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
     }
 
     @Override
-    public boolean isGoingUp(float displacement) {
-        return displacement > 0;
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement < 0 : displacement > 0;
+    }
+
+    @Override
+    public boolean isLayoutNaturalToLauncher() {
+        return false;
     }
 
     @Override
@@ -115,32 +120,21 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.height();
-    }
-
-    @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 ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationY = dp.heightPx - view.getPaddingTop();
-        return new ScaleAndTranslation(1f, 0f, offscreenTranslationY);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationY;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_Y;
     }
@@ -157,11 +151,6 @@
     }
 
     @Override
-    public float getViewCenterPosition(View view) {
-        return view.getTop() + view.getTranslationY();
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollY();
     }
@@ -187,16 +176,8 @@
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
-        if (displayRotation == Surface.ROTATION_0) {
-            rect.offset(0, value);
-        } else if (displayRotation == Surface.ROTATION_90) {
-            rect.offset(value, 0);
-        } else if (displayRotation == Surface.ROTATION_180) {
-            rect.offset(0, -value);
-        } else {
-            rect.offset(-value, 0);
-        }
+    public int getRotation() {
+        return Surface.ROTATION_90;
     }
 
     @Override
@@ -205,6 +186,11 @@
     }
 
     @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;
@@ -226,13 +212,40 @@
     }
 
     @Override
-    public int getShortEdgeLength(DeviceProfile dp) {
-        return dp.heightPx;
+    public int getTaskDismissDirectionFactor() {
+        return 1;
     }
 
     @Override
-    public int getTaskDismissDirectionFactor() {
-        return 1;
+    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
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 24fa815..65b1a7a 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -18,6 +18,7 @@
 
 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;
@@ -26,9 +27,9 @@
 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.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.util.OverScroller;
 
@@ -39,6 +40,10 @@
  */
 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);
     }
@@ -48,34 +53,34 @@
     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);
-    int getPrimarySize(Rect rect);
     float getPrimarySize(RectF rect);
+    int getClearAllScrollOffset(View view, boolean isRtl);
     int getSecondaryDimension(View view);
-    LauncherState.ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view);
-    float getTranslationValue(LauncherState.ScaleAndTranslation scaleAndTranslation);
     FloatProperty<View> getPrimaryViewTranslate();
     FloatProperty<View> getSecondaryViewTranslate();
     void setPrimaryAndResetSecondaryTranslate(View view, float translation);
-    float getViewCenterPosition(View view);
     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 getShortEdgeLength(DeviceProfile dp);
     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();
-    void offsetTaskRect(RectF rect, float value, int delta);
+    int getRotation();
     int getPrimaryValue(int x, int y);
     int getSecondaryValue(int x, int y);
     void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll);
@@ -83,8 +88,14 @@
     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 mInsets, CurveProperties out);
-    boolean isGoingUp(float displacement);
+    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
@@ -92,7 +103,6 @@
      */
     void adjustFloatingIconStartVelocity(PointF velocity);
 
-
     class CurveProperties {
         public int scroll;
         public int halfPageSize;
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 6d903b3..79e5c87 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -30,9 +30,8 @@
 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.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -65,19 +64,25 @@
     }
 
     @Override
-    public void getCurveProperties(PagedView view, Rect mInsets, CurveProperties out) {
+    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 = mInsets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
+        out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
     }
 
     @Override
-    public boolean isGoingUp(float displacement) {
+    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
     }
@@ -113,32 +118,21 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.width();
-    }
-
-    @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 ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationX = dp.widthPx - view.getPaddingStart();
-        return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationX;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_X;
     }
@@ -155,11 +149,6 @@
     }
 
     @Override
-    public float getViewCenterPosition(View view) {
-        return view.getLeft() + view.getTranslationX();
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollX();
     }
@@ -185,16 +174,8 @@
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
-        if (displayRotation == Surface.ROTATION_0) {
-            rect.offset(value, 0);
-        } else if (displayRotation == Surface.ROTATION_90) {
-            rect.offset(0, -value);
-        } else if (displayRotation == Surface.ROTATION_180) {
-            rect.offset(-value, 0);
-        } else {
-            rect.offset(0, value);
-        }
+    public int getRotation() {
+        return Surface.ROTATION_0;
     }
 
     @Override
@@ -203,6 +184,11 @@
     }
 
     @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;
@@ -224,13 +210,39 @@
     }
 
     @Override
-    public int getShortEdgeLength(DeviceProfile dp) {
-        return dp.widthPx;
+    public int getTaskDismissDirectionFactor() {
+        return -1;
     }
 
     @Override
-    public int getTaskDismissDirectionFactor() {
-        return -1;
+    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
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index eebd87f..d5ae2dc 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -18,8 +18,8 @@
 
 import android.content.res.Resources;
 import android.graphics.PointF;
-import android.graphics.RectF;
 import android.view.Surface;
+import android.view.View;
 
 import com.android.launcher3.Utilities;
 
@@ -31,21 +31,13 @@
     }
 
     @Override
-    public boolean getRecentsRtlSetting(Resources resources) {
-        return Utilities.isRtl(resources);
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        return isRtl ? -1 : 1;
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
-        if (displayRotation == Surface.ROTATION_0) {
-            rect.offset(0, value);
-        } else if (displayRotation == Surface.ROTATION_90) {
-            rect.offset(value, 0);
-        } else if (displayRotation == Surface.ROTATION_180) {
-            rect.offset(0, -value);
-        } else {
-            rect.offset(-value, 0);
-        }
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return Utilities.isRtl(resources);
     }
 
     @Override
@@ -54,8 +46,13 @@
     }
 
     @Override
-    public boolean isGoingUp(float displacement) {
-        return displacement < 0;
+    public int getRotation() {
+        return Surface.ROTATION_270;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement > 0 : displacement < 0;
     }
 
     @Override
@@ -64,4 +61,20 @@
         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 d725486..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).
@@ -103,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) {
@@ -154,6 +161,10 @@
 
     @Override
     protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector "
+                    + mListener.getClass().getSimpleName());
+        }
         mListener.onDrag(mDir.extractDirection(displacement),
                 mDir.extractOrthogonalDirection(displacement), event);
     }
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index da631bd..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;
@@ -41,8 +42,6 @@
 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
@@ -167,17 +166,15 @@
 
     @Override
     public void onLongPress(MotionEvent event) {
-        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Workspace.longPress");
         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
index 499f655..59266b4 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -15,27 +15,23 @@
  */
 package com.android.launcher3.util;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.util.Log;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
-import com.android.launcher3.testing.TestProtocol;
 
 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> implements Runnable {
+public final class ActivityTracker<T extends BaseActivity> {
 
     private WeakReference<T> mCurrentActivity = new WeakReference<>(null);
-    private WeakReference<SchedulerCallback<T>> mPendingCallback = new WeakReference<>(null);
 
     private static final String EXTRA_SCHEDULER_CALLBACK = "launcher.scheduler_callback";
 
@@ -51,76 +47,32 @@
     }
 
     /**
-     * Schedules the callback to be notified when the activity is created.
-     * @return true if the activity is already created, false otherwise
+     * 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 boolean schedule(SchedulerCallback<? extends T> callback) {
-        synchronized (this) {
-            mPendingCallback = new WeakReference<>((SchedulerCallback<T>) callback);
-        }
-        if (!notifyInitIfPending()) {
-            // If the activity doesn't already exist, then post and wait for the activity to start
-            MAIN_EXECUTOR.execute(this);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public void run() {
-        notifyInitIfPending();
-    }
-
-    /**
-     * Notifies the pending callback if the activity is now created.
-     * @return true if the activity is now created.
-     */
-    private boolean notifyInitIfPending() {
+    public void runCallbackWhenActivityExists(SchedulerCallback<T> callback, Intent intent) {
         T activity = mCurrentActivity.get();
         if (activity != null) {
-            notifyInitIfPending(activity, activity.isStarted());
-            return true;
+            callback.init(activity, activity.isStarted());
+        } else {
+            callback.addToIntent(intent);
         }
-        return false;
-    }
-
-    public boolean notifyInitIfPending(T activity, boolean alreadyOnHome) {
-        SchedulerCallback<T> pendingCallback = mPendingCallback.get();
-        if (pendingCallback != null) {
-            if (!pendingCallback.init(activity, alreadyOnHome)) {
-                clearReference(pendingCallback);
-            }
-            return true;
-        }
-        return false;
-    }
-
-    public boolean clearReference(SchedulerCallback<? extends T> handler) {
-        synchronized (this) {
-            if (mPendingCallback.get() == handler) {
-                mPendingCallback.clear();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    public boolean hasPending() {
-        return mPendingCallback.get() != null;
     }
 
     public boolean handleCreate(T activity) {
         mCurrentActivity = new WeakReference<>(activity);
-        return handleIntent(activity, activity.getIntent(), false, false);
+        return handleIntent(activity, activity.getIntent(), false);
     }
 
     public boolean handleNewIntent(T activity, Intent intent) {
-        return handleIntent(activity, intent, activity.isStarted(), true);
+        return handleIntent(activity, intent, activity.isStarted());
     }
 
-    private boolean handleIntent(
-            T activity, Intent intent, boolean alreadyOnHome, boolean explicitIntent) {
-        boolean result = false;
+    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) {
@@ -129,19 +81,26 @@
                 if (!handler.init(activity, alreadyOnHome)) {
                     intent.getExtras().remove(EXTRA_SCHEDULER_CALLBACK);
                 }
-                result = true;
+                return true;
             }
         }
-        if (!result && !explicitIntent) {
-            result = notifyInitIfPending(activity, alreadyOnHome);
-        }
-        return result;
+        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));
diff --git a/src/com/android/launcher3/util/DefaultDisplay.java b/src/com/android/launcher3/util/DefaultDisplay.java
index f18e411..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,6 @@
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
-import android.view.WindowManager;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -46,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
@@ -75,7 +83,7 @@
         }
 
         Info oldInfo = mInfo;
-        Info info = new Info(mContext);
+        Info info = new Info(mDisplayContext);
 
         int change = 0;
         if (info.hasDifferentSize(oldInfo)) {
@@ -142,10 +150,11 @@
         }
 
         private Info(Context context) {
-            this(context.getSystemService(WindowManager.class).getDefaultDisplay());
+            this(context, context.getSystemService(DisplayManager.class)
+                    .getDisplay(DEFAULT_DISPLAY));
         }
 
-        public Info(Display display) {
+        public Info(Context context, Display display) {
             id = display.getDisplayId();
             rotation = display.getRotation();
 
@@ -158,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/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/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 b54074e..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.
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/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 34efb12..87e6986 100644
--- a/src/com/android/launcher3/util/OverScroller.java
+++ b/src/com/android/launcher3/util/OverScroller.java
@@ -561,10 +561,11 @@
             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);
@@ -576,9 +577,9 @@
                 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 6c18747..86f3431 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.util;
 
-import static android.content.pm.PackageInstaller.SessionInfo;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 
 import android.app.AppOpsManager;
@@ -45,16 +44,14 @@
 import android.util.Pair;
 import android.widget.Toast;
 
-import androidx.annotation.NonNull;
-
-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.Utilities;
-import com.android.launcher3.WorkspaceItemInfo;
+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;
@@ -348,15 +345,4 @@
         }
         return false;
     }
-
-    /**
-     * Returns the created time in millis of given session info. Returns 0 if not available.
-     */
-    public static long getSessionCreatedTimeInMillis(@NonNull final SessionInfo info) {
-        try {
-            return (long) SessionInfo.class.getDeclaredMethod("getCreatedMillis").invoke(info);
-        } catch (Exception e) {
-            return 0;
-        }
-    }
 }
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/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/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
index 48aa02b..4b22429 100644
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ b/src/com/android/launcher3/util/SecureSettingsObserver.java
@@ -29,6 +29,11 @@
 
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
     public static final String NOTIFICATION_BADGING = "notification_badging";
+    /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */
+    public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled";
+    /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */
+    public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED =
+            "swipe_bottom_to_notification_enabled";
 
     private final ContentResolver mResolver;
     private final String mKeySetting;
@@ -79,4 +84,21 @@
         return new SecureSettingsObserver(
                 context.getContentResolver(), listener, NOTIFICATION_BADGING, 1);
     }
+
+    public static SecureSettingsObserver newOneHandedSettingsObserver(Context context,
+            OnChangeListener listener) {
+        return new SecureSettingsObserver(
+                context.getContentResolver(), listener, ONE_HANDED_ENABLED, 1);
+    }
+
+    /**
+     * Constructs settings observer for {@link #ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED}
+     * preference.
+     */
+    public static SecureSettingsObserver newSwipeToNotificationSettingsObserver(Context context,
+            OnChangeListener listener) {
+        return new SecureSettingsObserver(
+                context.getContentResolver(), listener,
+                ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+    }
 }
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 49c97da..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.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() && !WidgetsModel.GO_DISABLE_WIDGETS;
+        return !isLoading && !info.isDisabled();
     }
 
     private static boolean isApp(ItemInfo info) {
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 da59afe..b74686f 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -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/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/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index c9cdeff..ae459e1 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -21,8 +21,8 @@
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
 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
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 60470dc..b4a6b14 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -32,7 +32,7 @@
 import androidx.core.content.ContextCompat;
 
 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.dragndrop.DragLayer;
@@ -48,13 +48,13 @@
     private static final long SHOW_DURATION_MS = 300;
     private static final long HIDE_DURATION_MS = 100;
 
-    protected final Launcher mLauncher;
+    protected final BaseDraggingActivity mActivity;
     private final Handler mHandler = new Handler();
     private Runnable mOnClosed;
 
     public ArrowTipView(Context context) {
         super(context, null, 0);
-        mLauncher = Launcher.getLauncher(context);
+        mActivity = BaseDraggingActivity.fromContext(context);
         init(context);
     }
 
@@ -75,11 +75,11 @@
                         .setStartDelay(0)
                         .setDuration(HIDE_DURATION_MS)
                         .setInterpolator(Interpolators.ACCEL)
-                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
+                        .withEndAction(() -> mActivity.getDragLayer().removeView(this))
                         .start();
             } else {
                 animate().cancel();
-                mLauncher.getDragLayer().removeView(this);
+                mActivity.getDragLayer().removeView(this);
             }
             if (mOnClosed != null) mOnClosed.run();
             mIsOpen = false;
@@ -125,13 +125,37 @@
      * 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);
-        mLauncher.getDragLayer().addView(this);
+        ViewGroup parent = mActivity.getDragLayer();
+        parent.addView(this);
 
         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
-        params.gravity = Gravity.CENTER_HORIZONTAL;
-        params.leftMargin = mLauncher.getDeviceProfile().workspacePadding.left;
-        params.rightMargin = mLauncher.getDeviceProfile().workspacePadding.right;
+        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()
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 2fc3eaf..b010b4b 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -90,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];
@@ -181,11 +191,17 @@
     }
 
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
-        if (isEventInLauncher(ev)) {
-            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
-            if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
-                return topView;
-            }
+        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
+                && (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
+                && topView.onControllerInterceptTouchEvent(ev)) {
+            return topView;
         }
 
         for (TouchController controller : mControllers) {
@@ -198,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);
@@ -275,24 +292,30 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "BaseDragLayer: " + ev);
-        }
         switch (ev.getAction()) {
             case ACTION_DOWN: {
-                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 (isEventInLauncher(ev)) {
-                    mTouchDispatchState &= ~TOUCH_DISPATCHING_GESTURE;
+                    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:
-                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);
@@ -302,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;
     }
 
     /**
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 478141a..1a8e11b 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -101,7 +101,6 @@
     private @Nullable Drawable mForeground;
     private @Nullable Drawable mBackground;
 
-    private boolean mIsVerticalBarLayout = false;
     private boolean mIsAdaptiveIcon = false;
 
     private ValueAnimator mRevealAnimator;
@@ -145,7 +144,8 @@
     }
 
     void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
-            boolean isOpening, float scale, float minSize, LayoutParams parentLp) {
+            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)
@@ -158,7 +158,7 @@
                 Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
                 LINEAR), 0, 1);
 
-        if (mIsVerticalBarLayout) {
+        if (isVerticalBarLayout) {
             mOutline.right = (int) (rect.width() / scale);
         } else {
             mOutline.bottom = (int) (rect.height() / scale);
@@ -183,16 +183,16 @@
                 mRevealAnimator.setCurrentFraction(shapeRevealProgress);
             }
 
-            float drawableScale = (mIsVerticalBarLayout ? mOutline.width() : mOutline.height())
+            float drawableScale = (isVerticalBarLayout ? mOutline.width() : mOutline.height())
                     / minSize;
-            setBackgroundDrawableBounds(drawableScale);
+            setBackgroundDrawableBounds(drawableScale, isVerticalBarLayout);
             if (isOpening) {
                 // Center align foreground
                 int height = mFinalDrawableBounds.height();
                 int width = mFinalDrawableBounds.width();
-                int diffY = mIsVerticalBarLayout ? 0
+                int diffY = isVerticalBarLayout ? 0
                         : (int) (((height * drawableScale) - height) / 2);
-                int diffX = mIsVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
+                int diffX = isVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
                         : 0;
                 sTmpRect.set(mFinalDrawableBounds);
                 sTmpRect.offset(diffX, diffY);
@@ -210,11 +210,11 @@
         invalidateOutline();
     }
 
-    private void setBackgroundDrawableBounds(float scale) {
+    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 (mIsVerticalBarLayout) {
+        if (isVerticalBarLayout) {
             sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
         } else {
             sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
@@ -228,7 +228,8 @@
         }
     }
 
-    void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening) {
+    void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
+            boolean isVerticalBarLayout) {
         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
         if (mIsAdaptiveIcon) {
             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
@@ -264,7 +265,7 @@
             }
 
             float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
-            if (mIsVerticalBarLayout) {
+            if (isVerticalBarLayout) {
                 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
             } else {
                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
@@ -285,7 +286,7 @@
                 bgDrawableStartScale = scale;
                 mOutline.set(0, 0, lp.width, lp.height);
             }
-            setBackgroundDrawableBounds(bgDrawableStartScale);
+            setBackgroundDrawableBounds(bgDrawableStartScale, isVerticalBarLayout);
             mEndRevealRect.set(0, 0, lp.width, lp.height);
             setOutlineProvider(new ViewOutlineProvider() {
                 @Override
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index ad8d69d..177aff4 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
 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;
@@ -50,7 +49,6 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -58,8 +56,10 @@
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.folder.FolderIcon;
 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.
@@ -100,6 +100,7 @@
 
     private AnimatorSet mFadeAnimatorSet;
     private ListenerView mListenerView;
+    private Runnable mFastFinishRunnable;
 
     public FloatingIconView(Context context) {
         this(context, null);
@@ -124,7 +125,6 @@
         super.onAttachedToWindow();
         if (!mIsOpening) {
             getViewTreeObserver().addOnGlobalLayoutListener(this);
-            mLauncher.getRotationHelper().setCurrentTransitionRequest(REQUEST_LOCK);
         }
     }
 
@@ -162,7 +162,7 @@
         float scale = Math.max(1f, Math.min(scaleX, scaleY));
 
         mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
-                minSize, lp);
+                minSize, lp, mIsVerticalBarLayout);
 
         setPivotX(0);
         setPivotY(0);
@@ -336,7 +336,7 @@
         final InsettableFrameLayout.LayoutParams lp =
                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
         mBadge = badge;
-        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening);
+        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout);
         if (drawable instanceof AdaptiveIconDrawable) {
             final int originalHeight = lp.height;
             final int originalWidth = lp.width;
@@ -444,9 +444,21 @@
         }
     }
 
+    /**
+     * 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();
@@ -549,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 = () -> {
@@ -628,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);
     }
@@ -656,6 +677,7 @@
         sTmpObjArray[0] = null;
         mIconLoadResult = null;
         mClipIconView.recycle();
+        mFastFinishRunnable = null;
     }
 
     private static class IconLoadResult {
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 880f123..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,13 +38,16 @@
 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.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;
@@ -66,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);
@@ -89,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) {
@@ -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));
+                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 39e1eac..22faf97 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -27,6 +27,7 @@
 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;
@@ -53,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;
@@ -62,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;
@@ -77,12 +79,11 @@
 
 import java.util.List;
 
-
 /**
  * Simple scrim which draws a flat color
  */
 public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
-        AccessibilityStateChangeListener, StateListener {
+        AccessibilityStateChangeListener {
 
     public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
             new IntProperty<ScrimView>("dragHandleAlpha") {
@@ -115,6 +116,17 @@
     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;
 
@@ -146,6 +158,7 @@
         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;
 
@@ -177,7 +190,7 @@
     @Override
     public void setInsets(Rect insets) {
         updateDragHandleBounds();
-        updateDragHandleVisibility(null);
+        updateDragHandleVisibility();
     }
 
     @Override
@@ -223,6 +236,7 @@
             mProgress = progress;
             stopDragHandleEducationAnim();
             updateColors();
+            updateSysUiColors();
             updateDragHandleAlpha();
             invalidate();
         }
@@ -235,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);
@@ -374,19 +399,23 @@
 
     @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) {
+    private void updateDragHandleVisibility(@Nullable Drawable recycle) {
         boolean visible = shouldDragHandleBeVisible();
         boolean wasVisible = mDragHandle != null;
         if (visible != wasVisible) {
@@ -424,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;
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
index d849138..d35a38f 100644
--- a/src/com/android/launcher3/views/WorkEduView.java
+++ b/src/com/android/launcher3/views/WorkEduView.java
@@ -32,18 +32,19 @@
 import com.android.launcher3.Insettable;
 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.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 {
+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";
@@ -82,6 +83,12 @@
     }
 
     @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.
     }
@@ -150,6 +157,7 @@
     private void show() {
         attachToContainer();
         animateOpen();
+        mLauncher.getStateManager().addStateListener(this);
     }
 
     @Override
@@ -177,8 +185,8 @@
     /**
      * Checks if user has not seen onboarding UI yet and shows it when user navigates to all apps
      */
-    public static LauncherStateManager.StateListener showEduFlowIfNeeded(Launcher launcher,
-            @Nullable LauncherStateManager.StateListener oldListener) {
+    public static StateListener<LauncherState> showEduFlowIfNeeded(Launcher launcher,
+            @Nullable StateListener<LauncherState> oldListener) {
         if (oldListener != null) {
             launcher.getStateManager().removeStateListener(oldListener);
         }
@@ -187,12 +195,7 @@
             return null;
         }
 
-        LauncherStateManager.StateListener listener = new LauncherStateManager.StateListener() {
-            @Override
-            public void onStateTransitionStart(LauncherState toState) {
-
-            }
-
+        StateListener<LauncherState> listener = new StateListener<LauncherState>() {
             @Override
             public void onStateTransitionComplete(LauncherState finalState) {
                 if (finalState != LauncherState.ALL_APPS) return;
@@ -227,4 +230,9 @@
     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/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 23c2160..3e5113a 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -28,10 +28,10 @@
 
 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;
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 78acc34..6f2e179 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,8 +19,6 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.PointF;
-import android.graphics.Rect;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
@@ -35,14 +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.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.dragndrop.DraggableView;
+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;
@@ -51,7 +48,7 @@
  * {@inheritDoc}
  */
 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
-        implements TouchCompleteListener, View.OnLongClickListener, DraggableView {
+        implements TouchCompleteListener, View.OnLongClickListener {
 
     // Related to the auto-advancing of widgets
     private static final long ADVANCE_INTERVAL = 20000;
@@ -73,15 +70,7 @@
     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);
@@ -307,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);
@@ -357,18 +326,4 @@
         }
         return false;
     }
-
-    @Override
-    public int getViewType() {
-        return DRAGGABLE_WIDGET;
-    }
-
-    @Override
-    public void getVisualDragBounds(Rect bounds) {
-        int x = (int) (1 - getScaleToFit()) * getMeasuredWidth() / 2;
-        int y = (int) (1 - getScaleToFit()) * getMeasuredWidth() / 2;
-        int width = (int) getScaleToFit() * getMeasuredWidth();
-        int height = (int) getScaleToFit() * getMeasuredHeight();
-        bounds.set(x, y , x + width, y + height);
-    }
 }
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 104ad77..ed42bc4 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -18,12 +18,14 @@
 
 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;
@@ -32,7 +34,21 @@
  * Extension of AppWidgetHostView with support for controlled keyboard navigation.
  */
 public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
-        implements DraggableView {
+        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;
@@ -137,13 +153,75 @@
         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 getVisualDragBounds(Rect bounds) {
-        bounds.set(0, 0 , getMeasuredWidth(), getMeasuredHeight());
+    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 6e21a41..9601652 100644
--- a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 
@@ -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 895f8de..9021d9e 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -37,12 +37,12 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-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;
 
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 4a0b4ef..bef91d2 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -55,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;
@@ -104,7 +104,7 @@
     }
 
     private void setContainerWidth() {
-        mCellSize = (int) (mDeviceProfile.cellWidthPx * WIDTH_SCALE);
+        mCellSize = (int) (mDeviceProfile.allAppsIconSizePx * WIDTH_SCALE);
         mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
     }
 
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
index f3c7822..4b6c569 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -28,10 +28,10 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetInfo;
 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;
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 8d1a3b0..30be7a6 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -32,12 +32,12 @@
 import android.widget.TextView;
 
 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.PendingAnimation;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.List;
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index f3b325d..df6e2c3 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -21,7 +21,7 @@
 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;
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index b3e9734..68a3ec5 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -39,10 +38,8 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 
@@ -71,14 +68,6 @@
 
     }
 
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsFullSheet: " + ev);
-        }
-        return super.dispatchTouchEvent(ev);
-    }
-
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -143,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,
@@ -164,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);
 
@@ -193,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) {
@@ -202,7 +192,6 @@
                 }
             });
             post(() -> {
-                mRecyclerView.setLayoutFrozen(true);
                 mOpenCloseAnimator.start();
                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
             });
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 7ec6214..82d4110 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -158,23 +158,13 @@
                     mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset);
         }
         if (mTouchDownOnScroller) {
-            final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 1 " + result);
-            }
-            return result;
-        }
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 2 false");
+            return mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
         return false;
     }
 
     @Override
     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView.onTouchEvent");
-        }
         if (mTouchDownOnScroller) {
             mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
@@ -182,42 +172,5 @@
 
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onRequestDisallowInterceptTouchEvent "
-                    + disallowIntercept);
-        }
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        final boolean result = super.dispatchTouchEvent(ev);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView: state: "
-                    + getScrollState()
-                    + " can scroll: " + getLayoutManager().canScrollVertically()
-                    + " result: " + result
-                    + " layout suppressed: " + isLayoutSuppressed()
-                    + " event: " + ev);
-        }
-        return result;
-    }
-
-    @Override
-    public void stopNestedScroll() {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "stopNestedScroll");
-        }
-        super.stopNestedScroll();
-    }
-
-    @Override
-    public void setLayoutFrozen(boolean frozen) {
-        if (frozen != isLayoutSuppressed()) {
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
-                        + " @ " + android.util.Log.getStackTraceString(new Throwable()));
-            }
-        }
-        super.setLayoutFrozen(frozen);
     }
 }
\ 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 0ea7d85..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;
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/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/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 28a9193..a434d07 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.plugins;
 
+import android.view.MotionEvent;
+
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
@@ -28,7 +30,7 @@
 public interface OverscrollPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
-    int VERSION = 3;
+    int VERSION = 4;
 
     String DEVICE_STATE_LOCKED = "Locked";
     String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -41,33 +43,33 @@
     boolean isActive();
 
     /**
-     * Called when a touch is down and has been recognized as an overscroll gesture.
-     * A call of this method will always result in `onTouchUp` being called, and possibly
-     * `onFling` as well.
-     *
+     * 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 onTouchStart(String deviceState, String underlyingActivity);
+    void onTouchEvent(MotionEvent event,
+                      int horizontalDistancePx,
+                      int verticalDistancePx,
+                      int thresholdPx,
+                      int flingDistanceThresholdPx,
+                      int flingVelocityThresholdPx,
+                      String deviceState,
+                      String underlyingActivity);
 
     /**
-     * Called when a touch that was previously recognized has moved.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if overscroll gesture handling should override all other gestures.
      */
-    void onTouchTraveled(int px);
+    boolean blockOtherGestures();
 
     /**
-     * Called when a touch that was previously recognized has ended.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if the overscroll gesture can pan the underlying app.
      */
-    void onTouchEnd(int px);
-
-    /**
-     * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
-     */
-    void onFling(float velocity);
+    boolean allowsUnderlyingActivityOverscroll();
 }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 0b99e7a..9d87788 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -24,6 +24,7 @@
 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;
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java b/src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
similarity index 64%
rename from src_ui_overrides/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
index 4913cad..4893c17 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
@@ -13,15 +13,17 @@
  * 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.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 
-/** Render preview using surface view. */
-public class PreviewSurfaceRenderer {
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 
-    /** Handle a received surface view request. */
-    public static void render(Context context, Bundle bundle) { }
+/** 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/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
index a56801f..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,10 +15,10 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 
-import com.android.launcher3.AbstractFloatingView;
+import android.content.Context;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -31,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
@@ -45,21 +45,11 @@
     }
 
     @Override
-    public int getTransitionDuration(Launcher context) {
+    public int getTransitionDuration(Context context) {
         return 320;
     }
 
     @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);
-    }
-
-    @Override
     public String getDescription(Launcher launcher) {
         return launcher.getString(R.string.all_apps_button_label);
     }
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 e20b2ca..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,8 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import com.android.launcher3.Launcher;
+import android.content.Context;
+
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
@@ -29,7 +30,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Launcher context) {
+    public int getTransitionDuration(Context context) {
         return 250;
     }
 
@@ -44,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 a9fff8e..4d1bfa6 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -62,7 +62,11 @@
     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 1c8f095..f243f27 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -29,6 +29,7 @@
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            android:exported="true"
             android:label="No Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -39,6 +40,7 @@
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWdigetHidden"
+            android:exported="true"
             android:label="Hidden widget">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -49,6 +51,7 @@
 
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
+            android:exported="true"
             android:label="With Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -58,12 +61,14 @@
         </receiver>
 
         <activity
-            android:name="com.android.launcher3.testcomponent.WidgetConfigActivity">
+            android:name="com.android.launcher3.testcomponent.WidgetConfigActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
             </intent-filter>
         </activity>
-        <activity android:name="com.android.launcher3.testcomponent.CustomShortcutConfigActivity">
+        <activity android:name="com.android.launcher3.testcomponent.CustomShortcutConfigActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.CREATE_SHORTCUT" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -72,6 +77,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
             android:icon="@drawable/test_drawable_pin_item"
+            android:exported="true"
             android:label="Test Pin Item">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -102,6 +108,7 @@
             android:stateNotNeeded="true"
             android:taskAffinity=""
             android:theme="@android:style/Theme.DeviceDefault.Light"
+            android:exported="true"
             android:windowSoftInputMode="adjustPan">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -114,6 +121,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.BaseTestingActivity"
             android:label="LauncherTestApp"
+            android:exported="true"
             android:taskAffinity="com.android.launcher3.testcomponent.Affinity1">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -128,6 +136,7 @@
         </activity>
         <activity-alias android:name="Activity2"
                         android:label="TestActivity2"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -136,6 +145,7 @@
         </activity-alias>
         <activity-alias android:name="Activity3"
                         android:label="TestActivity3"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -144,6 +154,7 @@
         </activity-alias>
         <activity-alias android:name="Activity4"
                         android:label="TestActivity4"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -152,6 +163,7 @@
         </activity-alias>
         <activity-alias android:name="Activity5"
                         android:label="TestActivity5"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -160,6 +172,7 @@
         </activity-alias>
         <activity-alias android:name="Activity6"
                         android:label="TestActivity6"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -168,6 +181,7 @@
         </activity-alias>
         <activity-alias android:name="Activity7"
                         android:label="TestActivity7"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -176,6 +190,7 @@
         </activity-alias>
         <activity-alias android:name="Activity8"
                         android:label="TestActivity8"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -184,6 +199,7 @@
         </activity-alias>
         <activity-alias android:name="Activity9"
                         android:label="TestActivity9"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -192,6 +208,7 @@
         </activity-alias>
         <activity-alias android:name="Activity10"
                         android:label="TestActivity10"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
@@ -200,6 +217,7 @@
         </activity-alias>
         <activity-alias android:name="Activity11"
                         android:label="TestActivity11"
+                        android:exported="true"
                         android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/tests/dummy_app/AndroidManifest.xml b/tests/dummy_app/AndroidManifest.xml
index f00138c..d5e2320 100644
--- a/tests/dummy_app/AndroidManifest.xml
+++ b/tests/dummy_app/AndroidManifest.xml
@@ -26,6 +26,7 @@
         <activity
             android:name="Activity1"
             android:icon="@mipmap/ic_launcher1"
+            android:exported="true"
             android:label="Aardwolf">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
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/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 873f1cb..858e183 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -17,7 +17,6 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -26,7 +25,6 @@
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -39,6 +37,8 @@
 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;
@@ -47,20 +47,20 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.ItemInfo;
 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;
 import com.android.launcher3.Utilities;
+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.ContentWriter;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Wait;
@@ -100,9 +100,10 @@
     public static final long DEFAULT_UI_TIMEOUT = 10000;
     private static final String TAG = "AbstractLauncherUiTest";
 
-    private static String sDetectedActivityLeak;
+    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());
@@ -115,30 +116,51 @@
         if (TestHelpers.isInLauncherProcess()) {
             StrictMode.VmPolicy.Builder builder =
                     new StrictMode.VmPolicy.Builder()
-                            .detectActivityLeaks()
+// b/154772063
+//                            .detectActivityLeaks()
                             .penaltyLog()
                             .penaltyListener(Runnable::run, violation -> {
-                                // Runs in the main thread. We can't dumpheap in the main thread,
-                                // so let's just mark the fact that the leak has happened.
-                                if (sDetectedActivityLeak == null) {
-                                    sDetectedActivityLeak = violation.toString();
-                                    try {
-                                        Debug.dumpHprofData(
-                                                getInstrumentation().getTargetContext()
-                                                        .getFilesDir().getPath()
-                                                        + "/ActivityLeakHeapDump.hprof");
-                                    } catch (Throwable e) {
-                                        Log.e(TAG, "dumpHprofData failed", e);
-                                    }
+                                if (sStrictmodeDetectedActivityLeak == null) {
+                                    sStrictmodeDetectedActivityLeak = violation.toString() + ", "
+                                            + dumpHprofData() + ".";
                                 }
                             });
             StrictMode.setVmPolicy(builder.build());
         }
     }
 
-    public static void checkDetectedLeaks() {
-        if (sDetectedActivityLeak != null && !sActivityLeakReported) {
+    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";
         }
     }
 
@@ -162,7 +184,6 @@
         mLauncher.enableDebugTracing();
         // Avoid double-reporting of Launcher crashes.
         mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
-        mLauncher.disableSensorRotation();
     }
 
     protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
@@ -257,6 +278,15 @@
         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
@@ -264,22 +294,15 @@
         // Limits UI tests affecting tests running after them.
         mLauncher.waitForLauncherInitialized();
         if (mLauncherPid != 0) {
-            assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
+            assertEquals("Launcher crashed, pid mismatch:",
+                    mLauncherPid, mLauncher.getPid().intValue());
         }
-        checkDetectedLeaks();
+        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();
-            mLauncherPid = mLauncher.getPid();
-            mLauncher.disableSensorRotation();
-        }
+    protected void clearLauncherData() {
+        mLauncher.clearLauncherData();
+        mLauncher.waitForLauncherInitialized();
     }
 
     /**
@@ -308,26 +331,7 @@
      * Adds {@param item} on the homescreen on the 0th screen
      */
     protected void addItemToScreen(ItemInfo item) {
-        ContentResolver resolver = mTargetContext.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(mTargetContext);
-        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(mTargetContext));
+        WidgetUtils.addItemToScreen(item, mTargetContext);
         resetLoaderState();
 
         // Launch the home activity
@@ -526,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/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 38f50c1..266f0ae 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -56,7 +56,7 @@
             private void evaluateInPortrait() throws Throwable {
                 mTest.mDevice.setOrientationNatural();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
-                AbstractLauncherUiTest.checkDetectedLeaks();
+                AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher);
                 base.evaluate();
                 mTest.getDevice().pressHome();
             }
@@ -64,7 +64,7 @@
             private void evaluateInLandscape() throws Throwable {
                 mTest.mDevice.setOrientationLeft();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
-                AbstractLauncherUiTest.checkDetectedLeaks();
+                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 de1ada4..34e425d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -64,7 +64,7 @@
         test.waitForResumed("Launcher internal state is still Background");
         // Check that we switched to home.
         test.mLauncher.getWorkspace();
-        AbstractLauncherUiTest.checkDetectedLeaks();
+        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher);
     }
 
     // Please don't add negative test cases for methods that fail only after a long wait.
@@ -314,7 +314,7 @@
                 switchToAllApps();
         allApps.freeze();
         try {
-            allApps.getAppIcon(APP_NAME).dragToWorkspace(false);
+            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
             mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
@@ -342,7 +342,7 @@
                     getMenuItem(0);
             final String shortcutName = menuItem.getText();
 
-            menuItem.dragToWorkspace(false);
+            menuItem.dragToWorkspace(false, false);
             mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index d93915c..9d4ccff 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,10 +28,10 @@
 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;
@@ -94,7 +94,7 @@
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
         widgets.
                 getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())).
-                dragToWorkspace(true);
+                dragToWorkspace(true, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 788e041..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(false);
+                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(false);
+                .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 001a88f..a4aa9f2 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -17,18 +17,16 @@
 
 import static androidx.test.InstrumentationRegistry.getTargetContext;
 
-import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
+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;
-import android.content.Context;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
@@ -39,18 +37,16 @@
 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.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.rule.ShellCommandRule;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
@@ -108,7 +104,7 @@
     @Test
     public void testBindNormalWidget_withConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
         addItemToScreen(item);
         verifyWidgetPresent(info);
@@ -117,7 +113,7 @@
     @Test
     public void testBindNormalWidget_withoutConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
         addItemToScreen(item);
         verifyWidgetPresent(info);
@@ -126,7 +122,7 @@
     @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;
 
         addItemToScreen(item);
@@ -147,7 +143,7 @@
         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;
 
         addItemToScreen(item);
@@ -160,7 +156,7 @@
         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;
 
         addItemToScreen(item);
@@ -269,60 +265,17 @@
 
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
         final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT);
-        if (widget == null) mLauncher.dumpViewHierarchy(); // b/152645831
         assertTrue("Widget is not present",
                 widget != null);
     }
 
     private void verifyPendingWidgetPresent() {
         final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT);
-        if (widget == null) mLauncher.dumpViewHierarchy(); // b/152645831
         assertTrue("Pending widget is not present",
                 widget != 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
-     */
-    public static LauncherAppWidgetInfo createWidgetInfo(
-            LauncherAppWidgetProviderInfo info, boolean bindWidget) {
-        Context targetContext = getTargetContext();
-
-        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;
-    }
-
-    /**
      * Returns a LauncherAppWidgetInfo with package name which is not present on the device
      */
     private LauncherAppWidgetInfo getInvalidWidgetInfo() {
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 0246f95..0e43d81 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -29,12 +29,12 @@
 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;
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 5daac39..e1fde3b 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -19,6 +19,7 @@
 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;
@@ -33,11 +34,14 @@
 
     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() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             if (mLauncher.getNavigationModel()
@@ -53,6 +57,7 @@
             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 808be66..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;
@@ -244,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/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index bdfd563..5de5b4a 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -31,7 +31,6 @@
  */
 public final class AppIcon extends Launchable {
 
-    private static final Pattern START_EVENT = Pattern.compile("start:");
     private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
 
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
@@ -64,6 +63,6 @@
 
     @Override
     protected void expectActivityStartEvents() {
-        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, START_EVENT);
+        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 37a7b91..a40919b 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -27,8 +27,6 @@
  */
 public class AppIconMenuItem extends Launchable {
 
-    private static final Pattern START_SHORTCUT_EVENT = Pattern.compile("start: shortcut:");
-
     AppIconMenuItem(LauncherInstrumentation launcher, UiObject2 shortcut) {
         super(launcher, shortcut);
     }
@@ -51,6 +49,6 @@
 
     @Override
     protected void expectActivityStartEvents() {
-        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, START_SHORTCUT_EVENT);
+        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 80b8e89..ce94a3e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -71,7 +71,6 @@
     }
 
     protected void goToOverviewUnchecked() {
-        final boolean launcherWasVisible = mLauncher.isLauncherVisible();
         switch (mLauncher.getNavigationModel()) {
             case ZERO_BUTTON: {
                 final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
@@ -87,6 +86,11 @@
                         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(
@@ -138,11 +142,6 @@
                 break;
         }
         expectSwitchToOverviewEvents();
-
-        if (!launcherWasVisible) {
-            mLauncher.expectEvent(
-                    TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START_ACTIVITY);
-        }
     }
 
     private void expectSwitchToOverviewEvents() {
@@ -192,11 +191,6 @@
                 }
                 final boolean isZeroButton = mLauncher.getNavigationModel()
                         == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
-                if (!launcherWasVisible) {
-                    mLauncher.expectEvent(
-                            TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_START_ACTIVITY);
-                }
                 mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
                         launcherWasVisible && isZeroButton
                                 ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
@@ -208,11 +202,6 @@
                 // Double press the recents button.
                 UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                if (!launcherWasVisible) {
-                    mLauncher.expectEvent(
-                            TestProtocol.SEQUENCE_MAIN,
-                            LauncherInstrumentation.EVENT_START_ACTIVITY);
-                }
                 mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
                 mLauncher.getOverview();
                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
@@ -220,8 +209,6 @@
                 break;
         }
         mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-        mLauncher.expectEvent(
-                TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_STOP_ACTIVITY);
     }
 
     protected String getSwipeHeightRequestName() {
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 69afcc4..223ae29 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -106,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(
+            return mLauncher.getDevice().findObjects(
                     mLauncher.getOverviewObjectSelector("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(mLauncher.getVisibleBounds(t1).width(),
-                            mLauncher.getVisibleBounds(t2).width()));
-
-            return new OverviewTask(mLauncher, widestTask, this);
         }
     }
+
+    /**
+     * 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/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 2922acf..13ecfb8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -25,8 +25,6 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.testing.TestProtocol;
-
 /**
  * Ancestor for AppIcon and AppMenuItem.
  */
@@ -64,8 +62,6 @@
                 event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
                 () -> "Launching an app didn't open a new window: " + mObject.getText());
         expectActivityStartEvents();
-        mLauncher.expectEvent(
-                TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_STOP_ACTIVITY);
 
         mLauncher.assertTrue(
                 "App didn't start: " + selector,
@@ -76,9 +72,11 @@
 
     /**
      * Drags an object to the center of homescreen.
-     * @param startsActivity whether it's expected to start an activity.
+     *
+     * @param startsActivity   whether it's expected to start an activity.
+     * @param isWidgetShortcut whether we drag a widget shortcut
      */
-    public void dragToWorkspace(boolean startsActivity) {
+    public void dragToWorkspace(boolean startsActivity, boolean isWidgetShortcut) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             final Point launchableCenter = getObject().getVisibleCenter();
             final Point displaySize = mLauncher.getRealDisplaySize();
@@ -93,6 +91,7 @@
                             displaySize.y / 2),
                     getLongPressIndicator(),
                     startsActivity,
+                    isWidgetShortcut,
                     () -> addExpectedEventsForLongClick());
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 975fe9c..f4274a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -28,6 +28,7 @@
 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;
@@ -39,6 +40,7 @@
 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;
@@ -97,8 +99,7 @@
     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_ACTIVITY = Pattern.compile("Activity\\.onStart");
-    static final Pattern EVENT_STOP_ACTIVITY = Pattern.compile("Activity\\.onStop");
+    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");
@@ -164,12 +165,9 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
-    private static LogEventChecker sEventChecker;
-    // True if there is an gesture in progress that needs event verification.
-    private static boolean sCheckingEvents;
+    private LogEventChecker mEventChecker;
 
     private boolean mCheckEventsForSuccessfulGestures = false;
-    private int mExpectedPid;
     private Runnable mOnLauncherCrashed;
 
     private static Pattern getTouchEventPattern(String prefix, String action) {
@@ -262,7 +260,12 @@
     }
 
     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() {
@@ -334,26 +337,31 @@
 
     private String getSystemAnomalyMessage() {
         try {
-            final StringBuilder sb = new StringBuilder();
+            {
+                final StringBuilder sb = new StringBuilder();
 
-            UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
-            if (object != null) {
-                sb.append("TITLE: ").append(object.getText());
-            }
+                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());
-            }
+                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 (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);
         }
@@ -361,33 +369,12 @@
         return null;
     }
 
-    private String getAnomalyMessage() {
-        if (mExpectedPid != 0 && mExpectedPid != getPid()) {
-            mExpectedPid = 0;
-            if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
-            return "Launcher crashed";
-        }
-
+    public void checkForAnomaly() {
         final String systemAnomalyMessage = getSystemAnomalyMessage();
         if (systemAnomalyMessage != null) {
-            return "http://go/tapl : Tests are broken by a non-Launcher system error: "
-                    + systemAnomalyMessage;
-        }
-
-        return null;
-    }
-
-    public void checkForAnomaly() {
-        final String anomalyMessage = getAnomalyMessage();
-        if (anomalyMessage != null) {
-            if (sCheckingEvents) {
-                sCheckingEvents = false;
-                sEventChecker.finishNoWait();
-            }
-            log("Hierarchy dump for: " + anomalyMessage);
-            dumpViewHierarchy();
-
-            Assert.fail(formatSystemHealthMessage(anomalyMessage));
+            Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
+                    "http://go/tapl : Tests are broken by a non-Launcher system error: "
+                            + systemAnomalyMessage, false)));
         }
     }
 
@@ -447,23 +434,51 @@
         return message;
     }
 
-    private void fail(String message) {
-        checkForAnomaly();
-
-        message = "http://go/tapl : " + getContextDescription() + message
-                + " (visible state: " + getVisibleStateMessage() + ")";
-        log("Hierarchy dump for: " + message);
-        dumpViewHierarchy();
-
-        if (sCheckingEvents) {
-            sCheckingEvents = false;
-            final String eventMismatch = sEventChecker.verify(0);
-            if (eventMismatch != null) {
-                message = message + ", having produced " + eventMismatch;
+    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();
             }
         }
 
-        Assert.fail(formatSystemHealthMessage(message));
+        dumpDiagnostics();
+
+        log("Hierarchy dump for: " + message);
+        dumpViewHierarchy();
+
+        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() {
@@ -534,13 +549,14 @@
                 mExpectedRotation, mDevice.getDisplayRotation());
 
         // b/148422894
+        String error = null;
         for (int i = 0; i != 600; ++i) {
-            if (getNavigationModeMismatchError() == null) break;
+            error = getNavigationModeMismatchError();
+            if (error == null) break;
             sleep(100);
         }
-
-        final String error = getNavigationModeMismatchError();
         assertTrue(error, error == null);
+
         log("verifyContainerType: " + containerType);
 
         final UiObject2 container = verifyVisibleObjects(containerType);
@@ -558,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 (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);
                 }
@@ -591,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:
@@ -639,6 +655,7 @@
      */
     public Workspace pressHome() {
         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
@@ -659,7 +676,7 @@
                             false, GestureScope.INSIDE_TO_OUTSIDE);
                     try (LauncherInstrumentation.Closable c = addContextLayer(
                             "Swiped up from context menu to home")) {
-                        waitUntilGone(CONTEXT_MENU_RES_ID);
+                        waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID);
                     }
                 }
                 if (hasLauncherObject(WORKSPACE_RES_ID)) {
@@ -667,7 +684,7 @@
                 } else {
                     log("Hierarchy before swiping up to home:");
                     dumpViewHierarchy();
-                    log(action = "swiping up to home from " + getVisibleStateMessage());
+                    action = "swiping up to home";
 
                     try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
                         swipeToState(
@@ -678,20 +695,12 @@
                                         ? GestureScope.INSIDE_TO_OUTSIDE
                                         : GestureScope.OUTSIDE);
                     }
-                    if (!launcherWasVisible) {
-                        expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_START_ACTIVITY);
-                    }
                 }
             } else {
-                if (!launcherWasVisible) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_START_ACTIVITY);
-                }
                 log("Hierarchy before clicking home:");
                 dumpViewHierarchy();
-                log(action = "clicking home button from " + getVisibleStateMessage());
+                action = "clicking home button";
                 try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
-                    mDevice.waitForIdle();
-
                     if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
                         expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
                         expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
@@ -703,7 +712,6 @@
                             !hasLauncherObject(WORKSPACE_RES_ID)
                                     && (hasLauncherObject(APPS_RES_ID)
                                     || hasLauncherObject(OVERVIEW_RES_ID)));
-                    mDevice.waitForIdle();
                 }
             }
             try (LauncherInstrumentation.Closable c = addContextLayer(
@@ -819,9 +827,17 @@
         }
     }
 
-    void waitUntilGone(String resId) {
-        assertTrue("Unexpected launcher object visible: " + resId,
-                mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
+    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));
     }
 
@@ -839,27 +855,42 @@
 
     @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 view in Launcher, 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 view in Launcher, 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;
+        }
     }
 
     private boolean hasLauncherObject(String resId) {
@@ -948,9 +979,9 @@
         executeAndWaitForEvent(
                 command,
                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
-                () -> "Failed to receive an event for the state change: expected "
+                () -> "Failed to receive an event for the state change: expected ["
                         + TestProtocol.stateOrdinalToString(expectedState)
-                        + ", actual: " + eventListToString(actualEvents));
+                        + "], actual: " + eventListToString(actualEvents));
     }
 
     private boolean isSwitchToStateEvent(
@@ -1150,7 +1181,8 @@
         }
 
         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();
     }
 
@@ -1263,7 +1295,7 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    public void disableSensorRotation() {
+    private void disableSensorRotation() {
         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
     }
 
@@ -1276,8 +1308,9 @@
                 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    public int getPid() {
-        return getTestInfo(TestProtocol.REQUEST_PID).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() {
@@ -1302,26 +1335,38 @@
         return tasks;
     }
 
+    public void clearLauncherData() {
+        getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
+    }
+
     public Closable eventsCheck() {
-        Assert.assertTrue("Nested event checking", !sCheckingEvents);
-        sCheckingEvents = true;
-        mExpectedPid = getPid();
-        if (sEventChecker == null) sEventChecker = new LogEventChecker();
-        sEventChecker.start();
+        Assert.assertTrue("Nested event checking", mEventChecker == null);
+        disableSensorRotation();
+        final Integer initialPid = getPid();
+        final LogEventChecker eventChecker = new LogEventChecker(this);
+        if (eventChecker.start()) mEventChecker = eventChecker;
 
         return () -> {
-            checkForAnomaly();
+            if (initialPid != null && initialPid.intValue() != getPid()) {
+                if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
+                checkForAnomaly();
+                Assert.fail(
+                        formatSystemHealthMessage(
+                                formatErrorWithEvents("Launcher crashed", false)));
+            }
 
-            if (sCheckingEvents) {
-                sCheckingEvents = false;
+            if (mEventChecker != null) {
+                mEventChecker = null;
                 if (mCheckEventsForSuccessfulGestures) {
-                    final String message = sEventChecker.verify(WAIT_TIME_MS);
+                    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 {
-                    sEventChecker.finishNoWait();
+                    eventChecker.finishNoWait();
                 }
             }
         };
@@ -1332,7 +1377,11 @@
     }
 
     void expectEvent(String sequence, Pattern expected) {
-        if (sCheckingEvents) sEventChecker.expectPattern(sequence, expected);
+        if (mEventChecker != null) {
+            mEventChecker.expectPattern(sequence, expected);
+        } else {
+            Log.d(TAG, "Expecting: " + sequence + " / " + expected);
+        }
     }
 
     Rect getVisibleBounds(UiObject2 object) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 49901ea..4440b82 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -15,147 +15,84 @@
  */
 package com.android.launcher3.tapl;
 
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-import android.util.Log;
+import android.os.SystemClock;
 
 import com.android.launcher3.testing.TestProtocol;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Semaphore;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * Utility class to read log on a background thread.
+ * Utility class to verify expected events.
  */
 public class LogEventChecker {
 
-    private static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
-            ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<sequence>[a-zA-Z]+) / (?<event>.*)");
-
-    private static final String START_PREFIX = "START_READER ";
-    private static final String FINISH_PREFIX = "FINISH_READER ";
-
-    private volatile CountDownLatch mFinished;
+    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<>();
 
-    private final ListMap<String> mEvents = new ListMap<>();
-    private final Semaphore mEventsCounter = new Semaphore(0);
-
-    private volatile String mStartCommand;
-    private volatile String mFinishCommand;
-
-    LogEventChecker() {
-        final Thread thread = new Thread(this::onRun, "log-reader-thread");
-        thread.setPriority(Thread.NORM_PRIORITY);
-        thread.start();
+    LogEventChecker(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
     }
 
-    void start() {
-        if (mFinished != null) {
-            try {
-                mFinished.await();
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            }
-            mFinished = null;
-        }
-        mEvents.clear();
+    boolean start() {
         mExpectedEvents.clear();
-        mEventsCounter.drainPermits();
-        final String id = UUID.randomUUID().toString();
-        mStartCommand = START_PREFIX + id;
-        mFinishCommand = FINISH_PREFIX + id;
-        Log.d(TestProtocol.TAPL_EVENTS_TAG, mStartCommand);
-    }
-
-    private void onRun() {
-        try {
-            // Note that we use Runtime.exec to start the log reading process instead of running
-            // it via UIAutomation, so that we can directly access the "Process" object and
-            // ensure that the instrumentation is not stuck forever.
-            final String cmd = "logcat -s " + TestProtocol.TAPL_EVENTS_TAG;
-
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                    Runtime.getRuntime().exec(cmd).getInputStream()))) {
-                for (;;) {
-                    // Skip everything before the next start command.
-                    for (;;) {
-                        final String event = reader.readLine();
-                        if (event.contains(TestProtocol.TAPL_EVENTS_TAG)
-                                && event.contains(mStartCommand)) {
-                            break;
-                        }
-                    }
-
-                    // Store all actual events until the finish command.
-                    for (;;) {
-                        final String event = reader.readLine();
-                        if (event.contains(TestProtocol.TAPL_EVENTS_TAG)) {
-                            if (event.contains(mFinishCommand)) {
-                                mFinished.countDown();
-                                break;
-                            } else {
-                                final Matcher matcher = EVENT_LOG_ENTRY.matcher(event);
-                                if (matcher.find()) {
-                                    mEvents.add(matcher.group("sequence"), matcher.group("event"));
-                                    mEventsCounter.release();
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_START_EVENT_LOGGING) != null;
     }
 
     void expectPattern(String sequence, Pattern pattern) {
         mExpectedEvents.add(sequence, pattern);
     }
 
-    private void finishSync(long waitForExpectedCountMs) {
-        try {
-            // Wait until Launcher generates the expected number of events.
-            int expectedCount = mExpectedEvents.entrySet()
+    // 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();
-            mEventsCounter.tryAcquire(expectedCount, waitForExpectedCountMs, MILLISECONDS);
-            finishNoWait();
-            mFinished.await();
-            mFinished = null;
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
+            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() {
-        mFinished = new CountDownLatch(1);
-        Log.d(TestProtocol.TAPL_EVENTS_TAG, mFinishCommand);
+        mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING);
     }
 
-    String verify(long waitForExpectedCountMs) {
-        finishSync(waitForExpectedCountMs);
+    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<>(mEvents.getNonNull(sequence));
+            List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
             final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
-            hasMismatches = hasMismatches || mismatchPosition != -1;
+            hasMismatches = hasMismatches
+                    || mismatchPosition != -1 && !ignoreMistatch(successfulGesture, sequence);
             formatSequenceWithMismatch(
                     sb,
                     sequence,
@@ -164,14 +101,15 @@
                     mismatchPosition);
         }
         // Check for unexpected event sequences in the actual data.
-        for (String actualNamedSequence : mEvents.keySet()) {
+        for (String actualNamedSequence : actualEvents.keySet()) {
             if (!mExpectedEvents.containsKey(actualNamedSequence)) {
-                hasMismatches = true;
+                hasMismatches = hasMismatches
+                        || !ignoreMistatch(successfulGesture, actualNamedSequence);
                 formatSequenceWithMismatch(
                         sb,
                         actualNamedSequence,
                         new ArrayList<>(),
-                        mEvents.get(actualNamedSequence),
+                        actualEvents.get(actualNamedSequence),
                         0);
             }
         }
@@ -179,6 +117,13 @@
         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) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
index 63a97f4..42b6bc9 100644
--- a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.tapl;
 
-import android.os.Build;
-
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
@@ -43,12 +41,7 @@
             LauncherInstrumentation.log("OptionsPopupMenuItem before click "
                     + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
             mLauncher.clickLauncherObject(mObject);
-            if (!Build.MODEL.contains("Cuttlefish") ||
-                    Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q &&
-                            !"R".equals(Build.VERSION.CODENAME)) {
-                mLauncher.expectEvent(
-                        TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_STOP_ACTIVITY);
-            }
+            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)),
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index fae5f19..b235919 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -79,8 +79,6 @@
                         () -> "Launching task didn't open a new window: "
                                 + mTask.getParent().getContentDescription());
                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-                mLauncher.expectEvent(
-                        TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_STOP_ACTIVITY);
             }
             return new Background(mLauncher);
         }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 5be57c6..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,7 @@
 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;
 
@@ -88,9 +91,12 @@
     }
 
     public Widget getWidget(String labelText) {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "getting widget " + labelText + " in widgets list")) {
+        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);
 
@@ -114,17 +120,17 @@
                         maxWidth = Math.max(mLauncher.getVisibleBounds(sibling).width(), maxWidth);
                     }
 
-                    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);
-                    }
-
                     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);
                     }
                 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index b3b5e32..f0e686f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -180,6 +180,7 @@
                                     mLauncher.getVisibleBounds(workspace).centerY()),
                             "deep_shortcuts_container",
                             false,
+                            false,
                             () -> mLauncher.expectEvent(
                                     TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
                     verifyActiveContainer();
@@ -202,7 +203,8 @@
 
     static void dragIconToWorkspace(
             LauncherInstrumentation launcher, Launchable launchable, Point dest,
-            String longPressIndicator, boolean startsActivity, Runnable expectLongClickEvents) {
+            String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut,
+            Runnable expectLongClickEvents) {
         LauncherInstrumentation.log("dragIconToWorkspace: begin");
         final Point launchableCenter = launchable.getObject().getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
@@ -224,12 +226,11 @@
                         downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
                         LauncherInstrumentation.GestureScope.INSIDE),
                 NORMAL_STATE_ORDINAL);
-        if (startsActivity) {
-            launcher.expectEvent(
-                    TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_STOP_ACTIVITY);
+        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");
     }
 
     /**